@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.
- package/bin/init.mjs +29 -0
- package/package.json +7 -2
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +7 -0
- package/template/.prettierrc +11 -0
- package/template/AGENTS.md +485 -0
- package/template/Logo/Exxat_Prism.svg +39 -0
- package/template/Logo/Exxat_one.svg +36 -0
- package/template/README.md +58 -0
- package/template/app/(app)/compliance/page.tsx +10 -0
- package/template/app/(app)/dashboard/loading.tsx +18 -0
- package/template/app/(app)/dashboard/page.tsx +36 -0
- package/template/app/(app)/data-list/[id]/page.tsx +28 -0
- package/template/app/(app)/data-list/new/page.tsx +31 -0
- package/template/app/(app)/data-list/page.tsx +10 -0
- package/template/app/(app)/error.tsx +43 -0
- package/template/app/(app)/help/page.tsx +34 -0
- package/template/app/(app)/layout.tsx +54 -0
- package/template/app/(app)/loading.tsx +18 -0
- package/template/app/(app)/question-bank/page.tsx +10 -0
- package/template/app/(app)/rotations/page.tsx +15 -0
- package/template/app/(app)/settings/page.tsx +17 -0
- package/template/app/(app)/sites/all/page.tsx +13 -0
- package/template/app/(app)/team/page.tsx +10 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +1811 -0
- package/template/app/layout.tsx +95 -0
- package/template/app/page.tsx +9 -0
- package/template/components/.gitkeep +0 -0
- package/template/components/app-sidebar-dynamic.tsx +15 -0
- package/template/components/app-sidebar.tsx +901 -0
- package/template/components/ask-leo-composer.tsx +216 -0
- package/template/components/ask-leo-sidebar.tsx +509 -0
- package/template/components/chart-area-interactive.tsx +293 -0
- package/template/components/charts-overview.tsx +2321 -0
- package/template/components/command-menu-01.tsx +133 -0
- package/template/components/command-menu-02.tsx +386 -0
- package/template/components/command-menu.tsx +182 -0
- package/template/components/compliance-board-view.tsx +134 -0
- package/template/components/compliance-client.tsx +92 -0
- package/template/components/compliance-list-view.tsx +59 -0
- package/template/components/compliance-page-header.tsx +89 -0
- package/template/components/compliance-table.tsx +525 -0
- package/template/components/dashboard-onboarding-gallery.tsx +13 -0
- package/template/components/dashboard-onboarding.tsx +21 -0
- package/template/components/dashboard-promo-banner.tsx +67 -0
- package/template/components/dashboard-quota-progress-card.tsx +369 -0
- package/template/components/dashboard-report-charts.tsx +69 -0
- package/template/components/dashboard-section-heading.tsx +68 -0
- package/template/components/dashboard-tabs.tsx +598 -0
- package/template/components/data-list-client.tsx +239 -0
- package/template/components/data-list-table-cells.test.tsx +22 -0
- package/template/components/data-list-table-cells.tsx +173 -0
- package/template/components/data-list-table.tsx +879 -0
- package/template/components/data-table/filter-date-calendar.tsx +38 -0
- package/template/components/data-table/filter-text-value-input.tsx +77 -0
- package/template/components/data-table/index.tsx +1612 -0
- package/template/components/data-table/pagination.tsx +256 -0
- package/template/components/data-table/types.ts +91 -0
- package/template/components/data-table/use-table-state.ts +566 -0
- package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
- package/template/components/data-view-dashboard-charts-team.tsx +968 -0
- package/template/components/data-view-dashboard-charts.tsx +1668 -0
- package/template/components/data-views/board-card-primitives.tsx +93 -0
- package/template/components/data-views/index.ts +41 -0
- package/template/components/data-views/list-page-board-card.tsx +192 -0
- package/template/components/data-views/list-page-board-template.tsx +122 -0
- package/template/components/data-views/placement-board-card.tsx +262 -0
- package/template/components/export-drawer.tsx +375 -0
- package/template/components/exxat-product-logo.tsx +453 -0
- package/template/components/form-layout-01.tsx +131 -0
- package/template/components/getting-started.tsx +625 -0
- package/template/components/key-metrics.tsx +920 -0
- package/template/components/leo-insight-indicator.tsx +364 -0
- package/template/components/leo-typing-dots.tsx +121 -0
- package/template/components/list-hub-status-badge.tsx +51 -0
- package/template/components/list-page-dashboard-charts.tsx +18 -0
- package/template/components/nav-documents.tsx +89 -0
- package/template/components/nav-main.tsx +58 -0
- package/template/components/nav-secondary.tsx +64 -0
- package/template/components/nav-user.tsx +190 -0
- package/template/components/new-placement-back-btn.tsx +28 -0
- package/template/components/new-placement-form.tsx +1066 -0
- package/template/components/onboarding/index.ts +4 -0
- package/template/components/onboarding/onboarding-01.tsx +7 -0
- package/template/components/onboarding/onboarding-02.tsx +7 -0
- package/template/components/onboarding/onboarding-03.tsx +7 -0
- package/template/components/onboarding/onboarding-04.tsx +7 -0
- package/template/components/page-header.tsx +57 -0
- package/template/components/placement-detail.tsx +438 -0
- package/template/components/placements-board-view.tsx +404 -0
- package/template/components/placements-list-view.tsx +285 -0
- package/template/components/placements-page-header.tsx +160 -0
- package/template/components/placements-table-columns.tsx +639 -0
- package/template/components/product-switcher.tsx +116 -0
- package/template/components/question-bank-board-view.tsx +205 -0
- package/template/components/question-bank-client.tsx +77 -0
- package/template/components/question-bank-list-view.tsx +59 -0
- package/template/components/question-bank-page-header.tsx +89 -0
- package/template/components/question-bank-table.tsx +586 -0
- package/template/components/rotations-empty-state.tsx +47 -0
- package/template/components/rotations-panel-activator.tsx +8 -0
- package/template/components/secondary-nav.tsx +394 -0
- package/template/components/secondary-panel.tsx +239 -0
- package/template/components/section-cards.tsx +106 -0
- package/template/components/settings-appearance-card.tsx +424 -0
- package/template/components/settings-client.tsx +537 -0
- package/template/components/settings-form-row.tsx +42 -0
- package/template/components/sidebar-auto-collapse.tsx +23 -0
- package/template/components/sidebar-auto-open.tsx +18 -0
- package/template/components/sidebar-shell.tsx +37 -0
- package/template/components/site-header.tsx +93 -0
- package/template/components/sites-all-client.tsx +154 -0
- package/template/components/sites-board-view.tsx +67 -0
- package/template/components/sites-list-view.tsx +47 -0
- package/template/components/sites-table.tsx +312 -0
- package/template/components/system-banner-slot.tsx +66 -0
- package/template/components/table-properties/column-row.tsx +90 -0
- package/template/components/table-properties/draggable-list.ts +49 -0
- package/template/components/table-properties/drawer-button.tsx +231 -0
- package/template/components/table-properties/drawer.tsx +1102 -0
- package/template/components/table-properties/filter-card.tsx +251 -0
- package/template/components/table-properties/index.ts +22 -0
- package/template/components/table-properties/sort-card.tsx +59 -0
- package/template/components/table-properties/types.ts +124 -0
- package/template/components/task-list-panel.tsx +98 -0
- package/template/components/task-priority-badge.tsx +28 -0
- package/template/components/team-board-view.tsx +114 -0
- package/template/components/team-client.tsx +93 -0
- package/template/components/team-list-view.tsx +62 -0
- package/template/components/team-page-header.tsx +92 -0
- package/template/components/team-table.tsx +525 -0
- package/template/components/templates/list-page.tsx +576 -0
- package/template/components/templates/primary-page-template.tsx +56 -0
- package/template/components/theme-color-sync.tsx +32 -0
- package/template/components/theme-provider.tsx +71 -0
- package/template/components/tinted-icon-disc.tsx +53 -0
- package/template/components/ui/ai-thinking-surface.tsx +121 -0
- package/template/components/ui/avatar.tsx +1 -0
- package/template/components/ui/badge.tsx +1 -0
- package/template/components/ui/banner.tsx +1 -0
- package/template/components/ui/breadcrumb.tsx +1 -0
- package/template/components/ui/button.tsx +1 -0
- package/template/components/ui/calendar.tsx +1 -0
- package/template/components/ui/card.tsx +1 -0
- package/template/components/ui/chart.tsx +1 -0
- package/template/components/ui/checkbox.tsx +1 -0
- package/template/components/ui/coach-mark.tsx +1 -0
- package/template/components/ui/collapsible.tsx +1 -0
- package/template/components/ui/command.tsx +1 -0
- package/template/components/ui/date-picker-field.tsx +1 -0
- package/template/components/ui/dialog.tsx +1 -0
- package/template/components/ui/dot-pattern.tsx +159 -0
- package/template/components/ui/drag-handle-grip.tsx +1 -0
- package/template/components/ui/drawer.tsx +1 -0
- package/template/components/ui/dropdown-menu.tsx +1 -0
- package/template/components/ui/field.tsx +1 -0
- package/template/components/ui/form.tsx +1 -0
- package/template/components/ui/input-group.tsx +1 -0
- package/template/components/ui/input-mask.tsx +1 -0
- package/template/components/ui/input.tsx +1 -0
- package/template/components/ui/kbd.tsx +1 -0
- package/template/components/ui/label.tsx +1 -0
- package/template/components/ui/leo-icon.tsx +726 -0
- package/template/components/ui/payment-card-fields.tsx +1 -0
- package/template/components/ui/popover.tsx +1 -0
- package/template/components/ui/radio-group.tsx +1 -0
- package/template/components/ui/select.tsx +1 -0
- package/template/components/ui/selection-tile-grid.tsx +1 -0
- package/template/components/ui/separator.tsx +1 -0
- package/template/components/ui/sheet.tsx +1 -0
- package/template/components/ui/sidebar.tsx +1 -0
- package/template/components/ui/skeleton.tsx +1 -0
- package/template/components/ui/sonner.tsx +1 -0
- package/template/components/ui/status-badge.tsx +1 -0
- package/template/components/ui/table.tsx +1 -0
- package/template/components/ui/tabs.tsx +1 -0
- package/template/components/ui/textarea.tsx +1 -0
- package/template/components/ui/tip.tsx +1 -0
- package/template/components/ui/toggle-group.tsx +1 -0
- package/template/components/ui/toggle-switch.tsx +1 -0
- package/template/components/ui/toggle.tsx +1 -0
- package/template/components/ui/tooltip.tsx +1 -0
- package/template/components/ui/view-segmented-control.tsx +1 -0
- package/template/components.json +27 -0
- package/template/contexts/chart-variant-context.tsx +35 -0
- package/template/contexts/command-menu-context.tsx +28 -0
- package/template/contexts/dashboard-view-context.tsx +35 -0
- package/template/contexts/product-context.tsx +38 -0
- package/template/contexts/system-banner-context.tsx +127 -0
- package/template/docs/command-menu-pattern.md +45 -0
- package/template/docs/data-views-pattern.md +160 -0
- package/template/ecosystem.config.cjs +20 -0
- package/template/eslint.config.mjs +18 -0
- package/template/fontawesome-subset.manifest.json +190 -0
- package/template/hooks/.gitkeep +0 -0
- package/template/hooks/use-app-theme.ts +1 -0
- package/template/hooks/use-coach-mark.ts +1 -0
- package/template/hooks/use-mobile.ts +1 -0
- package/template/hooks/use-mod-key-label.ts +1 -0
- package/template/lib/.gitkeep +0 -0
- package/template/lib/ask-leo-route-context.ts +133 -0
- package/template/lib/chart-keyboard-selection.test.ts +20 -0
- package/template/lib/chart-keyboard-selection.ts +17 -0
- package/template/lib/chart-line-dash.ts +16 -0
- package/template/lib/coach-mark-registry.ts +68 -0
- package/template/lib/command-menu-config.ts +127 -0
- package/template/lib/command-menu-search-data.ts +44 -0
- package/template/lib/conditional-rule-match.ts +32 -0
- package/template/lib/dashboard-customize-coach-mark.ts +18 -0
- package/template/lib/dashboard-layout-merge.ts +63 -0
- package/template/lib/data-list-display-options.ts +35 -0
- package/template/lib/data-list-persistence.ts +280 -0
- package/template/lib/data-list-view-surface.ts +58 -0
- package/template/lib/data-list-view.ts +29 -0
- package/template/lib/data-view-dashboard-storage.ts +101 -0
- package/template/lib/date-filter.ts +8 -0
- package/template/lib/dev-log.test.ts +28 -0
- package/template/lib/dev-log.ts +8 -0
- package/template/lib/editable-target.ts +10 -0
- package/template/lib/floating-sheet-panel.ts +72 -0
- package/template/lib/initials-from-name.ts +7 -0
- package/template/lib/list-page-table-properties.ts +52 -0
- package/template/lib/list-status-badges.ts +168 -0
- package/template/lib/logo-dev.ts +12 -0
- package/template/lib/mock/compliance-kpi.ts +61 -0
- package/template/lib/mock/compliance.ts +146 -0
- package/template/lib/mock/dashboard.ts +105 -0
- package/template/lib/mock/navigation.tsx +231 -0
- package/template/lib/mock/placements-kpi.ts +134 -0
- package/template/lib/mock/placements.ts +183 -0
- package/template/lib/mock/question-bank-kpi.ts +61 -0
- package/template/lib/mock/question-bank.ts +142 -0
- package/template/lib/mock/sites-directory.ts +16 -0
- package/template/lib/mock/sites-kpi.ts +25 -0
- package/template/lib/mock/team-kpi.ts +60 -0
- package/template/lib/mock/team.ts +118 -0
- package/template/lib/motion-ui.ts +17 -0
- package/template/lib/placement-board-card-layout.ts +79 -0
- package/template/lib/placement-lifecycle.ts +5 -0
- package/template/lib/row-height.ts +10 -0
- package/template/lib/stock-portrait.ts +11 -0
- package/template/lib/utils.test.ts +13 -0
- package/template/lib/utils.ts +1 -0
- package/template/next.config.mjs +15 -0
- package/template/package.json +83 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/.gitkeep +0 -0
- package/template/public/Illustration/Rotation.svg +74 -0
- package/template/public/avatars/user.svg +11 -0
- package/template/public/favicon/favicon.ico +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logos/exxat-one.svg +36 -0
- package/template/public/logos/exxat-prism.svg +39 -0
- package/template/public/mock-schools/emory.svg +4 -0
- package/template/public/mock-schools/rush.svg +4 -0
- package/template/scripts/fontawesome-subset-audit.mjs +190 -0
- package/template/scripts/pm2-startup-macos.sh +13 -0
- package/template/skills-lock.json +10 -0
- package/template/stores/app-store.ts +33 -0
- package/template/tests/setup.ts +1 -0
- package/template/tsconfig.json +35 -0
- package/template/types/react-payment-inputs.d.ts +19 -0
- 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
|
+
}
|