@exxatdesignux/ui 0.2.6 → 0.2.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/package.json +2 -1
- package/template/.agents/skills/shadcn/SKILL.md +242 -0
- package/template/.agents/skills/shadcn/agents/openai.yml +5 -0
- package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
- package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
- package/template/.agents/skills/shadcn/cli.md +257 -0
- package/template/.agents/skills/shadcn/customization.md +202 -0
- package/template/.agents/skills/shadcn/evals/evals.json +47 -0
- package/template/.agents/skills/shadcn/mcp.md +94 -0
- package/template/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/template/.agents/skills/shadcn/rules/composition.md +195 -0
- package/template/.agents/skills/shadcn/rules/forms.md +192 -0
- package/template/.agents/skills/shadcn/rules/icons.md +101 -0
- package/template/.agents/skills/shadcn/rules/styling.md +162 -0
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +712 -0
- package/template/.cursor/rules/exxat-accessibility.mdc +33 -0
- package/template/.cursor/rules/exxat-command-menu.mdc +23 -0
- package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +53 -0
- package/template/.cursor/rules/exxat-data-tables.mdc +31 -0
- package/template/.cursor/rules/exxat-ds-agents.mdc +26 -0
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +100 -0
- package/template/.cursor/rules/exxat-list-page-connected-views.mdc +16 -0
- package/template/.cursor/rules/exxat-no-toast.mdc +26 -0
- package/template/.cursor/rules/exxat-page-vs-drawer.mdc +22 -0
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +40 -0
- package/template/AGENTS.md +52 -11
- package/template/app/(app)/dashboard/page.tsx +1 -1
- package/template/app/(app)/data-list/[id]/page.tsx +24 -8
- package/template/app/(app)/data-list/new/page.tsx +7 -4
- package/template/app/(app)/data-list/page.tsx +1 -1
- package/template/app/(app)/examples/page.tsx +41 -0
- package/template/app/(app)/question-bank/page.tsx +3 -3
- package/template/app/globals.css +1 -1
- package/template/components/app-sidebar.tsx +52 -35
- package/template/components/compliance-table.tsx +79 -0
- package/template/components/data-list-client.tsx +36 -25
- package/template/components/data-list-table.tsx +797 -10
- package/template/components/data-views/finder-panel-view.tsx +405 -0
- package/template/components/data-views/folder-grid-view.tsx +86 -0
- package/template/components/data-views/index.ts +59 -0
- package/template/components/data-views/list-page-split-details-placeholder.tsx +39 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +60 -0
- package/template/components/data-views/list-page-split-hub-tokens.ts +16 -0
- package/template/components/data-views/list-page-tree-column-header.tsx +31 -0
- package/template/components/data-views/list-page-tree-panel-shell.tsx +91 -0
- package/template/components/data-views/list-page-view-frame.tsx +53 -0
- package/template/components/data-views/os-folder-glyph.tsx +121 -0
- package/template/components/folder-details-shell.tsx +230 -0
- package/template/components/hub-tree-panel-view.tsx +672 -0
- package/template/components/list-hub-status-badge.tsx +17 -3
- package/template/components/placements-page-header.tsx +14 -8
- package/template/components/placements-table-columns.tsx +8 -8
- package/template/components/question-bank-client.tsx +157 -40
- package/template/components/question-bank-new-folder-sheet.tsx +248 -0
- package/template/components/question-bank-os-folder-view.tsx +648 -0
- package/template/components/question-bank-page-header.tsx +3 -3
- package/template/components/question-bank-panel-activator.tsx +9 -0
- package/template/components/question-bank-secondary-nav.tsx +226 -0
- package/template/components/question-bank-table.tsx +707 -22
- package/template/components/secondary-panel.tsx +41 -107
- package/template/components/sites-table.tsx +66 -0
- package/template/components/team-client.tsx +7 -0
- package/template/components/team-table.tsx +156 -1
- package/template/components/templates/list-page.tsx +2 -2
- package/template/components/ui/avatar.tsx +1 -1
- package/template/components/ui/badge.tsx +1 -1
- package/template/components/ui/banner.tsx +1 -1
- package/template/components/ui/breadcrumb.tsx +1 -1
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/calendar.tsx +1 -1
- package/template/components/ui/card.tsx +1 -1
- package/template/components/ui/chart.tsx +1 -1
- package/template/components/ui/checkbox.tsx +1 -1
- package/template/components/ui/coach-mark.tsx +1 -1
- package/template/components/ui/collapsible.tsx +1 -1
- package/template/components/ui/command.tsx +1 -1
- package/template/components/ui/date-picker-field.tsx +1 -1
- package/template/components/ui/dialog.tsx +1 -1
- package/template/components/ui/drag-handle-grip.tsx +1 -1
- package/template/components/ui/drawer.tsx +1 -1
- package/template/components/ui/dropdown-menu.tsx +1 -1
- package/template/components/ui/field.tsx +1 -1
- package/template/components/ui/form.tsx +1 -1
- package/template/components/ui/input-group.tsx +1 -1
- package/template/components/ui/input-mask.tsx +1 -1
- package/template/components/ui/input.tsx +1 -1
- package/template/components/ui/kbd.tsx +1 -1
- package/template/components/ui/label.tsx +1 -1
- package/template/components/ui/payment-card-fields.tsx +1 -1
- package/template/components/ui/popover.tsx +1 -1
- package/template/components/ui/radio-group.tsx +1 -1
- package/template/components/ui/resizable.tsx +68 -0
- package/template/components/ui/select.tsx +1 -1
- package/template/components/ui/selection-tile-grid.tsx +1 -1
- package/template/components/ui/separator.tsx +1 -1
- package/template/components/ui/sheet.tsx +1 -1
- package/template/components/ui/sidebar.tsx +1 -1
- package/template/components/ui/skeleton.tsx +1 -1
- package/template/components/ui/sonner.tsx +1 -1
- package/template/components/ui/status-badge.tsx +1 -1
- package/template/components/ui/table.tsx +1 -1
- package/template/components/ui/tabs.tsx +1 -1
- package/template/components/ui/textarea.tsx +1 -1
- package/template/components/ui/tip.tsx +1 -1
- package/template/components/ui/toggle-group.tsx +1 -1
- package/template/components/ui/toggle-switch.tsx +1 -1
- package/template/components/ui/toggle.tsx +1 -1
- package/template/components/ui/tooltip.tsx +1 -1
- package/template/components/ui/view-segmented-control.tsx +1 -1
- package/template/docs/data-views-pattern.md +7 -0
- package/template/hooks/use-app-theme.ts +1 -1
- package/template/hooks/use-coach-mark.ts +1 -1
- package/template/hooks/use-location-hash.ts +15 -0
- package/template/hooks/use-mobile.ts +1 -1
- package/template/hooks/use-mod-key-label.ts +1 -1
- package/template/hooks/use-sidebar-reflow-zoom.ts +40 -0
- package/template/lib/ask-leo-route-context.ts +25 -57
- package/template/lib/coach-mark-registry.ts +13 -13
- package/template/lib/command-menu-config.ts +28 -23
- package/template/lib/command-menu-search-data.ts +10 -9
- package/template/lib/data-list-view-surface.ts +12 -1
- package/template/lib/data-list-view.ts +6 -3
- package/template/lib/date-filter.ts +1 -1
- package/template/lib/mock/dashboard.ts +11 -11
- package/template/lib/mock/navigation.tsx +22 -63
- package/template/lib/mock/placements-kpi.ts +19 -19
- package/template/lib/mock/question-bank-folders.ts +167 -0
- package/template/lib/mock/question-bank-inspector.ts +109 -0
- package/template/lib/mock/question-bank-kpi.ts +1 -1
- package/template/lib/mock/question-bank.ts +80 -0
- package/template/lib/question-bank-nav.ts +91 -0
- package/template/lib/utils.ts +1 -1
- package/template/next.config.mjs +8 -0
- package/template/package.json +1 -0
- package/template/public/folders/icons8-folder-windows-11.svg +1 -0
- package/template/app/(app)/compliance/page.tsx +0 -10
- package/template/app/(app)/rotations/page.tsx +0 -15
- package/template/app/(app)/sites/all/page.tsx +0 -13
- package/template/app/(app)/team/page.tsx +0 -10
|
@@ -5,18 +5,18 @@
|
|
|
5
5
|
import type { MetricItem, MetricInsight } from "@/components/key-metrics"
|
|
6
6
|
|
|
7
7
|
export const DASHBOARD_METRICS: MetricItem[] = [
|
|
8
|
-
{ id: "pending-requests", label: "
|
|
9
|
-
{ id: "confirmed-placements", label: "
|
|
10
|
-
{ id: "pending-reviews", label: "
|
|
11
|
-
{ id: "available-slots", label: "Available
|
|
12
|
-
{ id: "new-applications", label: "New
|
|
13
|
-
{ id: "compliance-rate", label: "
|
|
8
|
+
{ id: "pending-requests", label: "Open tasks", value: "23", delta: "+5", trend: "up", href: "/data-list" },
|
|
9
|
+
{ id: "confirmed-placements", label: "Active pipelines", value: "89", delta: "+12", trend: "up", href: "/data-list" },
|
|
10
|
+
{ id: "pending-reviews", label: "In review", value: "8", delta: "-3", trend: "down", href: "/data-list" },
|
|
11
|
+
{ id: "available-slots", label: "Available slots", value: "156", delta: "+24", trend: "up", href: "/data-list" },
|
|
12
|
+
{ id: "new-applications", label: "New items", value: "34", delta: "+7", trend: "up", href: "/data-list" },
|
|
13
|
+
{ id: "compliance-rate", label: "Health score", value: "98%", delta: "+2", trend: "up", href: "/data-list" },
|
|
14
14
|
]
|
|
15
15
|
|
|
16
16
|
export const DASHBOARD_INSIGHT: MetricInsight = {
|
|
17
|
-
title: "
|
|
18
|
-
description: "
|
|
19
|
-
href: "/
|
|
17
|
+
title: "Throughput note",
|
|
18
|
+
description: "Demo insight card — wire real KPIs from your product domain.",
|
|
19
|
+
href: "/examples",
|
|
20
20
|
severity: "warning",
|
|
21
21
|
actionLabel: "Ask Leo",
|
|
22
22
|
}
|
|
@@ -74,8 +74,8 @@ export interface DashboardStudentScoresData {
|
|
|
74
74
|
|
|
75
75
|
/** Example: student 75 on scale 50–80, class average 60 (same band). */
|
|
76
76
|
export const DASHBOARD_STUDENT_SCORES: DashboardStudentScoresData = {
|
|
77
|
-
title: "
|
|
78
|
-
description: "
|
|
77
|
+
title: "Sample scores",
|
|
78
|
+
description: "Reference chart: individual vs average on a fixed band (demo).",
|
|
79
79
|
metrics: [
|
|
80
80
|
{
|
|
81
81
|
id: "midterm",
|
|
@@ -87,88 +87,47 @@ export const NAV_PRIMARY: NavLinkItem[] = [
|
|
|
87
87
|
icon: <i className="fa-light fa-grid-2" aria-hidden="true" />,
|
|
88
88
|
iconActive: <i className="fa-solid fa-grid-2" aria-hidden="true" />,
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
key: "examples",
|
|
92
|
+
title: "Patterns",
|
|
93
|
+
url: "/examples",
|
|
94
|
+
icon: <i className="fa-light fa-layer-group" aria-hidden="true" />,
|
|
95
|
+
iconActive: <i className="fa-solid fa-layer-group" aria-hidden="true" />,
|
|
96
|
+
},
|
|
90
97
|
{
|
|
91
98
|
key: "question-bank",
|
|
92
99
|
title: "Question bank",
|
|
93
100
|
url: "/question-bank",
|
|
94
101
|
icon: <i className="fa-light fa-books" aria-hidden="true" />,
|
|
95
102
|
iconActive: <i className="fa-solid fa-books" aria-hidden="true" />,
|
|
103
|
+
secondaryPanel: "question-bank",
|
|
96
104
|
},
|
|
97
105
|
{
|
|
98
106
|
key: "data-list",
|
|
99
|
-
title: "
|
|
107
|
+
title: "List hub",
|
|
100
108
|
url: "/data-list",
|
|
101
|
-
icon: <i className="fa-light fa-
|
|
102
|
-
iconActive: <i className="fa-solid fa-
|
|
109
|
+
icon: <i className="fa-light fa-table" aria-hidden="true" />,
|
|
110
|
+
iconActive: <i className="fa-solid fa-table" aria-hidden="true" />,
|
|
103
111
|
badge: 24,
|
|
104
112
|
},
|
|
105
|
-
{
|
|
106
|
-
key: "rotations",
|
|
107
|
-
title: "Rotations",
|
|
108
|
-
url: "/rotations",
|
|
109
|
-
icon: <i className="fa-light fa-arrows-rotate" aria-hidden="true" />,
|
|
110
|
-
iconActive: <i className="fa-solid fa-arrows-rotate" aria-hidden="true" />,
|
|
111
|
-
secondaryPanel: "rotations",
|
|
112
|
-
primaryHubChildKey: "view-all-rotations",
|
|
113
|
-
children: [
|
|
114
|
-
{
|
|
115
|
-
key: "rotation-1",
|
|
116
|
-
title: "Clinical Nursing — Fall 2026",
|
|
117
|
-
url: "/rotations",
|
|
118
|
-
icon: <i className="fa-light fa-folder" aria-hidden="true" />,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
key: "rotation-2",
|
|
122
|
-
title: "PT Fieldwork — Spring 2026",
|
|
123
|
-
url: "/rotations",
|
|
124
|
-
icon: <i className="fa-light fa-folder" aria-hidden="true" />,
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
key: "rotation-3",
|
|
128
|
-
title: "OT Level II — Summer 2026",
|
|
129
|
-
url: "/rotations",
|
|
130
|
-
icon: <i className="fa-light fa-folder" aria-hidden="true" />,
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
key: "view-all-rotations",
|
|
134
|
-
title: "View all",
|
|
135
|
-
url: "/rotations",
|
|
136
|
-
icon: <i className="fa-light fa-arrow-right" aria-hidden="true" />,
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
key: "sites",
|
|
142
|
-
title: "Sites",
|
|
143
|
-
url: "/sites/all",
|
|
144
|
-
icon: <i className="fa-light fa-hospital" aria-hidden="true" />,
|
|
145
|
-
iconActive: <i className="fa-solid fa-hospital" aria-hidden="true" />,
|
|
146
|
-
children: Array.from({ length: 45 }, (_, i) => ({
|
|
147
|
-
key: `site-${i + 1}`,
|
|
148
|
-
title: `Site ${String(i + 1).padStart(2, "0")}`,
|
|
149
|
-
url: `/sites/all#site-${i + 1}`,
|
|
150
|
-
icon: <i className="fa-light fa-hospital" aria-hidden="true" />,
|
|
151
|
-
})),
|
|
152
|
-
},
|
|
153
113
|
]
|
|
154
114
|
|
|
155
115
|
// ── Documents section ───────────────────────────────────────────────────────
|
|
156
116
|
|
|
157
|
-
export const NAV_DOCUMENTS_LABEL = "
|
|
117
|
+
export const NAV_DOCUMENTS_LABEL = "Resources"
|
|
158
118
|
|
|
159
119
|
export const NAV_DOCUMENTS: NavLinkItem[] = [
|
|
160
120
|
{
|
|
161
|
-
key: "
|
|
162
|
-
title: "
|
|
163
|
-
url: "
|
|
164
|
-
icon: <i className="fa-light fa-
|
|
165
|
-
iconActive: <i className="fa-solid fa-
|
|
166
|
-
badge: "Beta",
|
|
121
|
+
key: "tokens",
|
|
122
|
+
title: "Tokens & themes",
|
|
123
|
+
url: "/settings",
|
|
124
|
+
icon: <i className="fa-light fa-palette" aria-hidden="true" />,
|
|
125
|
+
iconActive: <i className="fa-solid fa-palette" aria-hidden="true" />,
|
|
167
126
|
},
|
|
168
127
|
{
|
|
169
128
|
key: "more",
|
|
170
129
|
title: "More",
|
|
171
|
-
url: "
|
|
130
|
+
url: "/help",
|
|
172
131
|
icon: <i className="fa-light fa-ellipsis" aria-hidden="true" />,
|
|
173
132
|
iconActive: <i className="fa-solid fa-ellipsis" aria-hidden="true" />,
|
|
174
133
|
},
|
|
@@ -191,7 +150,7 @@ export interface NavSecondaryItem {
|
|
|
191
150
|
export const NAV_QUICK_ACTIONS: NavSecondaryItem[] = [
|
|
192
151
|
{
|
|
193
152
|
key: "command-menu",
|
|
194
|
-
title: "Search
|
|
153
|
+
title: "Search",
|
|
195
154
|
url: "#",
|
|
196
155
|
icon: <i className="fa-light fa-magnifying-glass" aria-hidden="true" />,
|
|
197
156
|
opensCommandMenu: true,
|
|
@@ -224,8 +183,8 @@ export const NAV_SECONDARY: NavSecondaryItem[] = [
|
|
|
224
183
|
// ── User ──────────────────────────────────────────────────────────────────────
|
|
225
184
|
|
|
226
185
|
export const NAV_USER = {
|
|
227
|
-
name: "
|
|
228
|
-
email: "
|
|
186
|
+
name: "Alex Morgan",
|
|
187
|
+
email: "alex.morgan@example.com",
|
|
229
188
|
/** Stock portrait (randomuser.me); stable for this seed */
|
|
230
|
-
avatar: stockPortraitUrl("exxat-nav-user-
|
|
189
|
+
avatar: stockPortraitUrl("exxat-nav-user-alex-morgan"),
|
|
231
190
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
//
|
|
2
|
+
// List hub — KPI strip + insight (data-list; dashboard view uses row-driven helpers)
|
|
3
3
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
import type { MetricInsight, MetricItem } from "@/components/key-metrics"
|
|
@@ -10,7 +10,7 @@ function statusCount(status: Status): number {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* KPIs from the current filtered
|
|
13
|
+
* KPIs from the current filtered row set (table/list/board/dashboard shared state).
|
|
14
14
|
* Use for the dashboard view tab; optional for the template metrics strip when you want parity.
|
|
15
15
|
*/
|
|
16
16
|
export function placementKpiMetricsFromRows(rows: Placement[]): MetricItem[] {
|
|
@@ -26,7 +26,7 @@ export function placementKpiMetricsFromRows(rows: Placement[]): MetricItem[] {
|
|
|
26
26
|
return [
|
|
27
27
|
{
|
|
28
28
|
id: "total-placements",
|
|
29
|
-
label: "Total
|
|
29
|
+
label: "Total rows",
|
|
30
30
|
value: total,
|
|
31
31
|
delta: "—",
|
|
32
32
|
trend: "neutral",
|
|
@@ -35,7 +35,7 @@ export function placementKpiMetricsFromRows(rows: Placement[]): MetricItem[] {
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
id: "starting-week",
|
|
38
|
-
label: "
|
|
38
|
+
label: "Due this week",
|
|
39
39
|
value: startingWeek,
|
|
40
40
|
delta: "—",
|
|
41
41
|
trend: startingWeek > 0 ? "up" : "neutral",
|
|
@@ -43,7 +43,7 @@ export function placementKpiMetricsFromRows(rows: Placement[]): MetricItem[] {
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
id: "compliance-alerts",
|
|
46
|
-
label: "
|
|
46
|
+
label: "Attention flags",
|
|
47
47
|
value: alerts,
|
|
48
48
|
delta: "—",
|
|
49
49
|
trend: alerts > 0 ? "up" : "neutral",
|
|
@@ -51,7 +51,7 @@ export function placementKpiMetricsFromRows(rows: Placement[]): MetricItem[] {
|
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
id: "avg-compliance",
|
|
54
|
-
label: "
|
|
54
|
+
label: "Completeness",
|
|
55
55
|
value: `${avgPct}%`,
|
|
56
56
|
delta: "—",
|
|
57
57
|
trend: "neutral",
|
|
@@ -65,24 +65,24 @@ export function placementKpiInsightFromRows(rows: Placement[]): MetricInsight {
|
|
|
65
65
|
const inReview = rows.filter(p => p.status === "under-review").length
|
|
66
66
|
const n = rows.length
|
|
67
67
|
return {
|
|
68
|
-
title: "
|
|
68
|
+
title: "Queue snapshot",
|
|
69
69
|
description:
|
|
70
70
|
n > 0
|
|
71
|
-
? `${pending} pending, ${inReview} in review in this view. Clear the queue to keep
|
|
72
|
-
: "No
|
|
73
|
-
href: "/
|
|
71
|
+
? `${pending} pending, ${inReview} in review in this view. Clear the queue to keep work moving.`
|
|
72
|
+
: "No rows match the current filters.",
|
|
73
|
+
href: "/data-list",
|
|
74
74
|
severity: pending + inReview > 0 ? "warning" : "info",
|
|
75
75
|
actionLabel: "Ask Leo",
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
80
|
+
* KPI row for the list hub metrics strip (demo numbers).
|
|
81
81
|
*/
|
|
82
82
|
export const PLACEMENT_KPI_METRICS: MetricItem[] = [
|
|
83
83
|
{
|
|
84
84
|
id: "total-placements",
|
|
85
|
-
label: "Total
|
|
85
|
+
label: "Total rows",
|
|
86
86
|
value: 50,
|
|
87
87
|
delta: "+12",
|
|
88
88
|
trend: "up",
|
|
@@ -91,7 +91,7 @@ export const PLACEMENT_KPI_METRICS: MetricItem[] = [
|
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
id: "starting-week",
|
|
94
|
-
label: "
|
|
94
|
+
label: "Due this week",
|
|
95
95
|
value: 0,
|
|
96
96
|
delta: "-5",
|
|
97
97
|
trend: "down",
|
|
@@ -99,7 +99,7 @@ export const PLACEMENT_KPI_METRICS: MetricItem[] = [
|
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
id: "compliance-alerts",
|
|
102
|
-
label: "
|
|
102
|
+
label: "Attention flags",
|
|
103
103
|
value: 23,
|
|
104
104
|
delta: "+13",
|
|
105
105
|
trend: "up",
|
|
@@ -107,7 +107,7 @@ export const PLACEMENT_KPI_METRICS: MetricItem[] = [
|
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
id: "avg-compliance",
|
|
110
|
-
label: "
|
|
110
|
+
label: "Completeness",
|
|
111
111
|
value: "87%",
|
|
112
112
|
delta: "+3",
|
|
113
113
|
trend: "up",
|
|
@@ -116,16 +116,16 @@ export const PLACEMENT_KPI_METRICS: MetricItem[] = [
|
|
|
116
116
|
]
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Insight copy
|
|
119
|
+
* Insight copy derived from demo row status counts.
|
|
120
120
|
*/
|
|
121
121
|
export function getPlacementInsight(): MetricInsight {
|
|
122
122
|
const pending = statusCount("pending")
|
|
123
123
|
const inReview = statusCount("under-review")
|
|
124
124
|
|
|
125
125
|
return {
|
|
126
|
-
title: "
|
|
127
|
-
description: `${pending} pending, ${inReview} in review. Clear the queue to keep
|
|
128
|
-
href: "/
|
|
126
|
+
title: "Queue snapshot",
|
|
127
|
+
description: `${pending} pending, ${inReview} in review. Clear the queue to keep work moving.`,
|
|
128
|
+
href: "/data-list",
|
|
129
129
|
severity: "warning",
|
|
130
130
|
actionLabel: "Ask Leo",
|
|
131
131
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Question bank folder tree (mock) — OS-style icon folders with appearance + hierarchy.
|
|
3
|
+
* Production: replace with API + optimistic updates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type QuestionBankFolderColorKey =
|
|
7
|
+
| "brand"
|
|
8
|
+
| "success"
|
|
9
|
+
| "warning"
|
|
10
|
+
| "destructive"
|
|
11
|
+
| "muted"
|
|
12
|
+
| "chart1"
|
|
13
|
+
| "chart2"
|
|
14
|
+
| "chart3"
|
|
15
|
+
|
|
16
|
+
export interface QuestionBankFolder {
|
|
17
|
+
id: string
|
|
18
|
+
name: string
|
|
19
|
+
/** `null` = top-level folder */
|
|
20
|
+
parentId: string | null
|
|
21
|
+
/** Font Awesome icon without weight prefix (e.g. `fa-folder`, `fa-flask`). */
|
|
22
|
+
icon: string
|
|
23
|
+
colorKey: QuestionBankFolderColorKey
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Tile + icon tint classes (semantic tokens). */
|
|
27
|
+
export const QUESTION_BANK_FOLDER_COLOR_STYLES: Record<
|
|
28
|
+
QuestionBankFolderColorKey,
|
|
29
|
+
{ tile: string; iconWrap: string; icon: string }
|
|
30
|
+
> = {
|
|
31
|
+
brand: {
|
|
32
|
+
tile: "border-brand/35 bg-brand/10",
|
|
33
|
+
iconWrap: "bg-brand/20",
|
|
34
|
+
icon: "text-brand",
|
|
35
|
+
},
|
|
36
|
+
success: {
|
|
37
|
+
tile: "border-emerald-500/35 bg-emerald-500/10",
|
|
38
|
+
iconWrap: "bg-emerald-500/15",
|
|
39
|
+
icon: "text-emerald-600 dark:text-emerald-400",
|
|
40
|
+
},
|
|
41
|
+
warning: {
|
|
42
|
+
tile: "border-amber-500/35 bg-amber-500/10",
|
|
43
|
+
iconWrap: "bg-amber-500/15",
|
|
44
|
+
icon: "text-amber-700 dark:text-amber-400",
|
|
45
|
+
},
|
|
46
|
+
destructive: {
|
|
47
|
+
tile: "border-destructive/35 bg-destructive/10",
|
|
48
|
+
iconWrap: "bg-destructive/15",
|
|
49
|
+
icon: "text-destructive",
|
|
50
|
+
},
|
|
51
|
+
muted: {
|
|
52
|
+
tile: "border-border bg-muted/50",
|
|
53
|
+
iconWrap: "bg-muted",
|
|
54
|
+
icon: "text-muted-foreground",
|
|
55
|
+
},
|
|
56
|
+
chart1: {
|
|
57
|
+
tile: "border-[color-mix(in_oklab,var(--color-chart-1)_40%,transparent)] bg-[color-mix(in_oklab,var(--color-chart-1)_12%,transparent)]",
|
|
58
|
+
iconWrap: "bg-[color-mix(in_oklab,var(--color-chart-1)_20%,transparent)]",
|
|
59
|
+
icon: "text-[var(--color-chart-1)]",
|
|
60
|
+
},
|
|
61
|
+
chart2: {
|
|
62
|
+
tile: "border-[color-mix(in_oklab,var(--color-chart-2)_40%,transparent)] bg-[color-mix(in_oklab,var(--color-chart-2)_12%,transparent)]",
|
|
63
|
+
iconWrap: "bg-[color-mix(in_oklab,var(--color-chart-2)_20%,transparent)]",
|
|
64
|
+
icon: "text-[var(--color-chart-2)]",
|
|
65
|
+
},
|
|
66
|
+
chart3: {
|
|
67
|
+
tile: "border-[color-mix(in_oklab,var(--color-chart-3)_40%,transparent)] bg-[color-mix(in_oklab,var(--color-chart-3)_12%,transparent)]",
|
|
68
|
+
iconWrap: "bg-[color-mix(in_oklab,var(--color-chart-3)_20%,transparent)]",
|
|
69
|
+
icon: "text-[var(--color-chart-3)]",
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Icon color classes using Tailwind — for use in text-based contexts (list views, panels). */
|
|
74
|
+
export const QUESTION_BANK_FOLDER_ICON_COLORS: Record<QuestionBankFolderColorKey, string> = {
|
|
75
|
+
brand: "text-orange-600 dark:text-orange-400",
|
|
76
|
+
success: "text-emerald-600 dark:text-emerald-400",
|
|
77
|
+
warning: "text-amber-600 dark:text-amber-400",
|
|
78
|
+
destructive: "text-red-600 dark:text-red-400",
|
|
79
|
+
muted: "text-slate-500 dark:text-slate-400",
|
|
80
|
+
chart1: "text-blue-600 dark:text-blue-400",
|
|
81
|
+
chart2: "text-lime-600 dark:text-lime-400",
|
|
82
|
+
chart3: "text-purple-600 dark:text-purple-400",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Preset icons for folder appearance picker. */
|
|
86
|
+
export const QUESTION_BANK_FOLDER_ICON_OPTIONS: readonly string[] = [
|
|
87
|
+
"fa-folder",
|
|
88
|
+
"fa-folder-open",
|
|
89
|
+
"fa-book",
|
|
90
|
+
"fa-flask",
|
|
91
|
+
"fa-stethoscope",
|
|
92
|
+
"fa-heart-pulse",
|
|
93
|
+
"fa-brain",
|
|
94
|
+
"fa-scale-balanced",
|
|
95
|
+
"fa-file-lines",
|
|
96
|
+
"fa-layer-group",
|
|
97
|
+
"fa-clipboard-check",
|
|
98
|
+
"fa-vial",
|
|
99
|
+
"fa-user-doctor",
|
|
100
|
+
"fa-kit-medical",
|
|
101
|
+
"fa-notes-medical",
|
|
102
|
+
] as const
|
|
103
|
+
|
|
104
|
+
export const DEFAULT_QUESTION_BANK_FOLDERS: QuestionBankFolder[] = [
|
|
105
|
+
{
|
|
106
|
+
id: "fld-clinical",
|
|
107
|
+
name: "Clinical",
|
|
108
|
+
parentId: null,
|
|
109
|
+
icon: "fa-stethoscope",
|
|
110
|
+
colorKey: "brand",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "fld-science",
|
|
114
|
+
name: "Basic science",
|
|
115
|
+
parentId: null,
|
|
116
|
+
icon: "fa-flask",
|
|
117
|
+
colorKey: "chart2",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "fld-ops",
|
|
121
|
+
name: "Operations",
|
|
122
|
+
parentId: null,
|
|
123
|
+
icon: "fa-clipboard-check",
|
|
124
|
+
colorKey: "warning",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "fld-ethics",
|
|
128
|
+
name: "Ethics & law",
|
|
129
|
+
parentId: null,
|
|
130
|
+
icon: "fa-scale-balanced",
|
|
131
|
+
colorKey: "muted",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "fld-skills-lab",
|
|
135
|
+
name: "Skills lab",
|
|
136
|
+
parentId: "fld-clinical",
|
|
137
|
+
icon: "fa-vial",
|
|
138
|
+
colorKey: "success",
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
export function newFolderId(): string {
|
|
143
|
+
return `fld-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function collectFolderDescendantIds(folders: QuestionBankFolder[], rootId: string): Set<string> {
|
|
147
|
+
const out = new Set<string>()
|
|
148
|
+
function walk(id: string) {
|
|
149
|
+
out.add(id)
|
|
150
|
+
for (const f of folders) {
|
|
151
|
+
if (f.parentId === id) walk(f.id)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
walk(rootId)
|
|
155
|
+
return out
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function isValidFolderMove(
|
|
159
|
+
folders: QuestionBankFolder[],
|
|
160
|
+
folderId: string,
|
|
161
|
+
newParentId: string | null,
|
|
162
|
+
): boolean {
|
|
163
|
+
if (folderId === newParentId) return false
|
|
164
|
+
if (newParentId === null) return true
|
|
165
|
+
const desc = collectFolderDescendantIds(folders, folderId)
|
|
166
|
+
return !desc.has(newParentId)
|
|
167
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived labels for the question bank inspector (mock — replace with API fields).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { QuestionBankDifficulty, QuestionBankItem, QuestionBankType } from "@/lib/mock/question-bank"
|
|
6
|
+
import type { QuestionBankFolder } from "@/lib/mock/question-bank-folders"
|
|
7
|
+
import { collectFolderDescendantIds } from "@/lib/mock/question-bank-folders"
|
|
8
|
+
|
|
9
|
+
export const QUESTION_TYPE_ABBREV: Record<QuestionBankType, string> = {
|
|
10
|
+
multiple_choice: "MCQ",
|
|
11
|
+
true_false: "T/F",
|
|
12
|
+
short_answer: "Short answer",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function deriveQuestionItemCode(q: QuestionBankItem): string {
|
|
16
|
+
const raw = q.itemCode?.trim()
|
|
17
|
+
if (raw) return raw
|
|
18
|
+
const n = q.id.replace(/\D/g, "") || "0"
|
|
19
|
+
return `QB-${String(n).padStart(3, "0")}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function deriveBloomLevel(q: QuestionBankItem): string {
|
|
23
|
+
if (q.bloomLevel && String(q.bloomLevel).trim()) return String(q.bloomLevel)
|
|
24
|
+
switch (q.difficulty) {
|
|
25
|
+
case "easy":
|
|
26
|
+
return "Remember"
|
|
27
|
+
case "medium":
|
|
28
|
+
return "Apply"
|
|
29
|
+
case "hard":
|
|
30
|
+
return "Analyze"
|
|
31
|
+
default:
|
|
32
|
+
return "Apply"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Relative “last edited” clause when `lastEditedSummary` is absent. */
|
|
37
|
+
export function deriveLastEditedLine(q: QuestionBankItem): string {
|
|
38
|
+
if (q.lastEditedSummary?.trim()) return q.lastEditedSummary.trim()
|
|
39
|
+
const editor = q.lastEditedBy ?? q.author
|
|
40
|
+
const d = new Date(q.updatedAt)
|
|
41
|
+
if (Number.isNaN(d.getTime())) return `Updated · ${editor}`
|
|
42
|
+
const ms = Date.now() - d.getTime()
|
|
43
|
+
const days = Math.floor(ms / (86400 * 1000))
|
|
44
|
+
if (days < 1) return `Today · ${editor}`
|
|
45
|
+
if (days < 14) return `${days} days ago · ${editor}`
|
|
46
|
+
const months = Math.floor(days / 30)
|
|
47
|
+
if (months < 24) return `${months} month${months === 1 ? "" : "s"} ago · ${editor}`
|
|
48
|
+
const years = Math.floor(months / 12)
|
|
49
|
+
return `${years} year${years === 1 ? "" : "s"} ago · ${editor}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function deriveTags(q: QuestionBankItem): string[] {
|
|
53
|
+
if (q.tags && q.tags.length > 0) return q.tags
|
|
54
|
+
return [q.topic].filter(Boolean)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Bloom taxonomy row order for folder aggregate charts. */
|
|
58
|
+
export const BLOOM_LEVEL_ORDER = [
|
|
59
|
+
"Remember",
|
|
60
|
+
"Understand",
|
|
61
|
+
"Apply",
|
|
62
|
+
"Analyze",
|
|
63
|
+
"Evaluate",
|
|
64
|
+
"Create",
|
|
65
|
+
] as const
|
|
66
|
+
|
|
67
|
+
export function questionsInFolderSubtree(
|
|
68
|
+
folders: QuestionBankFolder[],
|
|
69
|
+
questions: QuestionBankItem[],
|
|
70
|
+
folderId: string,
|
|
71
|
+
): QuestionBankItem[] {
|
|
72
|
+
const scope = collectFolderDescendantIds(folders, folderId)
|
|
73
|
+
return questions.filter(q => scope.has(q.folderId))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface FolderQuestionAggregate {
|
|
77
|
+
totalQuestions: number
|
|
78
|
+
difficulty: Record<QuestionBankDifficulty, number>
|
|
79
|
+
/** Counts keyed by Bloom label (includes derived levels). */
|
|
80
|
+
bloom: Record<string, number>
|
|
81
|
+
avgPbi: number | null
|
|
82
|
+
scoredCount: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function aggregateFolderQuestions(rows: QuestionBankItem[]): FolderQuestionAggregate {
|
|
86
|
+
const difficulty: Record<QuestionBankDifficulty, number> = {
|
|
87
|
+
easy: 0,
|
|
88
|
+
medium: 0,
|
|
89
|
+
hard: 0,
|
|
90
|
+
}
|
|
91
|
+
const bloom: Record<string, number> = {}
|
|
92
|
+
for (const label of BLOOM_LEVEL_ORDER) bloom[label] = 0
|
|
93
|
+
const pbis: number[] = []
|
|
94
|
+
for (const q of rows) {
|
|
95
|
+
difficulty[q.difficulty]++
|
|
96
|
+
const bl = deriveBloomLevel(q)
|
|
97
|
+
bloom[bl] = (bloom[bl] ?? 0) + 1
|
|
98
|
+
if (typeof q.pbi === "number" && !Number.isNaN(q.pbi)) pbis.push(q.pbi)
|
|
99
|
+
}
|
|
100
|
+
const scoredCount = pbis.length
|
|
101
|
+
const avgPbi = scoredCount ? pbis.reduce((a, n) => a + n, 0) / scoredCount : null
|
|
102
|
+
return {
|
|
103
|
+
totalQuestions: rows.length,
|
|
104
|
+
difficulty,
|
|
105
|
+
bloom,
|
|
106
|
+
avgPbi,
|
|
107
|
+
scoredCount,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -47,7 +47,7 @@ export function questionBankKpiInsight(rows: QuestionBankItem[]): MetricInsight
|
|
|
47
47
|
const review = rows.filter(r => r.status === "in_review").length
|
|
48
48
|
const draft = rows.filter(r => r.status === "draft").length
|
|
49
49
|
return {
|
|
50
|
-
title: "
|
|
50
|
+
title: "Folder library",
|
|
51
51
|
description:
|
|
52
52
|
review > 0
|
|
53
53
|
? `${review} item(s) in review. ${draft} draft(s) not yet published.`
|