@exxatdesignux/ui 0.1.0 → 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.
Files changed (155) hide show
  1. package/bin/cli.mjs +176 -0
  2. package/bin/init.mjs +15 -1
  3. package/bin/sync-extras.mjs +65 -0
  4. package/consumer-extras/README.md +21 -0
  5. package/consumer-extras/cursor-skills/exxat-accessibility/SKILL.md +282 -0
  6. package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +68 -0
  7. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +99 -0
  8. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +713 -0
  9. package/consumer-extras/cursor-skills/exxat-fontawesome-icons/SKILL.md +31 -0
  10. package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +36 -0
  11. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +27 -0
  12. package/consumer-extras/patterns/command-menu-pattern.md +45 -0
  13. package/consumer-extras/patterns/data-views-pattern.md +167 -0
  14. package/package.json +7 -3
  15. package/src/components/ui/sidebar.tsx +7 -2
  16. package/template/.agents/skills/shadcn/SKILL.md +242 -0
  17. package/template/.agents/skills/shadcn/agents/openai.yml +5 -0
  18. package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
  19. package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
  20. package/template/.agents/skills/shadcn/cli.md +257 -0
  21. package/template/.agents/skills/shadcn/customization.md +202 -0
  22. package/template/.agents/skills/shadcn/evals/evals.json +47 -0
  23. package/template/.agents/skills/shadcn/mcp.md +94 -0
  24. package/template/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
  25. package/template/.agents/skills/shadcn/rules/composition.md +195 -0
  26. package/template/.agents/skills/shadcn/rules/forms.md +192 -0
  27. package/template/.agents/skills/shadcn/rules/icons.md +101 -0
  28. package/template/.agents/skills/shadcn/rules/styling.md +162 -0
  29. package/template/.claude/skills/exxat-ds-skill/SKILL.md +712 -0
  30. package/template/.cursor/rules/exxat-accessibility.mdc +33 -0
  31. package/template/.cursor/rules/exxat-command-menu.mdc +23 -0
  32. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +53 -0
  33. package/template/.cursor/rules/exxat-data-tables.mdc +31 -0
  34. package/template/.cursor/rules/exxat-ds-agents.mdc +26 -0
  35. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +100 -0
  36. package/template/.cursor/rules/exxat-list-page-connected-views.mdc +16 -0
  37. package/template/.cursor/rules/exxat-no-toast.mdc +26 -0
  38. package/template/.cursor/rules/exxat-page-vs-drawer.mdc +22 -0
  39. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +40 -0
  40. package/template/AGENTS.md +52 -11
  41. package/template/app/(app)/dashboard/page.tsx +1 -1
  42. package/template/app/(app)/data-list/[id]/page.tsx +24 -8
  43. package/template/app/(app)/data-list/new/page.tsx +7 -4
  44. package/template/app/(app)/data-list/page.tsx +1 -1
  45. package/template/app/(app)/examples/page.tsx +41 -0
  46. package/template/app/(app)/question-bank/page.tsx +3 -3
  47. package/template/app/globals.css +1 -1
  48. package/template/components/app-sidebar.tsx +52 -35
  49. package/template/components/compliance-table.tsx +79 -0
  50. package/template/components/data-list-client.tsx +36 -25
  51. package/template/components/data-list-table.tsx +797 -10
  52. package/template/components/data-views/finder-panel-view.tsx +405 -0
  53. package/template/components/data-views/folder-grid-view.tsx +86 -0
  54. package/template/components/data-views/index.ts +59 -0
  55. package/template/components/data-views/list-page-split-details-placeholder.tsx +39 -0
  56. package/template/components/data-views/list-page-split-hub-chrome.tsx +60 -0
  57. package/template/components/data-views/list-page-split-hub-tokens.ts +16 -0
  58. package/template/components/data-views/list-page-tree-column-header.tsx +31 -0
  59. package/template/components/data-views/list-page-tree-panel-shell.tsx +91 -0
  60. package/template/components/data-views/list-page-view-frame.tsx +53 -0
  61. package/template/components/data-views/os-folder-glyph.tsx +121 -0
  62. package/template/components/folder-details-shell.tsx +230 -0
  63. package/template/components/hub-tree-panel-view.tsx +672 -0
  64. package/template/components/list-hub-status-badge.tsx +17 -3
  65. package/template/components/page-header.tsx +149 -7
  66. package/template/components/placements-page-header.tsx +14 -8
  67. package/template/components/placements-table-columns.tsx +8 -8
  68. package/template/components/question-bank-client.tsx +157 -39
  69. package/template/components/question-bank-new-folder-sheet.tsx +248 -0
  70. package/template/components/question-bank-os-folder-view.tsx +648 -0
  71. package/template/components/question-bank-page-header.tsx +31 -2
  72. package/template/components/question-bank-panel-activator.tsx +9 -0
  73. package/template/components/question-bank-secondary-nav.tsx +226 -0
  74. package/template/components/question-bank-table.tsx +707 -22
  75. package/template/components/secondary-panel.tsx +41 -107
  76. package/template/components/sites-table.tsx +66 -0
  77. package/template/components/team-client.tsx +7 -0
  78. package/template/components/team-table.tsx +156 -1
  79. package/template/components/templates/list-page.tsx +2 -2
  80. package/template/components/ui/avatar.tsx +1 -1
  81. package/template/components/ui/badge.tsx +1 -1
  82. package/template/components/ui/banner.tsx +1 -1
  83. package/template/components/ui/breadcrumb.tsx +1 -1
  84. package/template/components/ui/button.tsx +1 -1
  85. package/template/components/ui/calendar.tsx +1 -1
  86. package/template/components/ui/card.tsx +1 -1
  87. package/template/components/ui/chart.tsx +1 -1
  88. package/template/components/ui/checkbox.tsx +1 -1
  89. package/template/components/ui/coach-mark.tsx +1 -1
  90. package/template/components/ui/collapsible.tsx +1 -1
  91. package/template/components/ui/command.tsx +1 -1
  92. package/template/components/ui/date-picker-field.tsx +1 -1
  93. package/template/components/ui/dialog.tsx +1 -1
  94. package/template/components/ui/drag-handle-grip.tsx +1 -1
  95. package/template/components/ui/drawer.tsx +1 -1
  96. package/template/components/ui/dropdown-menu.tsx +1 -1
  97. package/template/components/ui/field.tsx +1 -1
  98. package/template/components/ui/form.tsx +1 -1
  99. package/template/components/ui/input-group.tsx +1 -1
  100. package/template/components/ui/input-mask.tsx +1 -1
  101. package/template/components/ui/input.tsx +1 -1
  102. package/template/components/ui/kbd.tsx +1 -1
  103. package/template/components/ui/label.tsx +1 -1
  104. package/template/components/ui/payment-card-fields.tsx +1 -1
  105. package/template/components/ui/popover.tsx +1 -1
  106. package/template/components/ui/radio-group.tsx +1 -1
  107. package/template/components/ui/resizable.tsx +68 -0
  108. package/template/components/ui/select.tsx +1 -1
  109. package/template/components/ui/selection-tile-grid.tsx +1 -1
  110. package/template/components/ui/separator.tsx +1 -1
  111. package/template/components/ui/sheet.tsx +1 -1
  112. package/template/components/ui/sidebar.tsx +1 -1
  113. package/template/components/ui/skeleton.tsx +1 -1
  114. package/template/components/ui/sonner.tsx +1 -1
  115. package/template/components/ui/status-badge.tsx +1 -1
  116. package/template/components/ui/table.tsx +1 -1
  117. package/template/components/ui/tabs.tsx +1 -1
  118. package/template/components/ui/textarea.tsx +1 -1
  119. package/template/components/ui/tip.tsx +1 -1
  120. package/template/components/ui/toggle-group.tsx +1 -1
  121. package/template/components/ui/toggle-switch.tsx +1 -1
  122. package/template/components/ui/toggle.tsx +1 -1
  123. package/template/components/ui/tooltip.tsx +1 -1
  124. package/template/components/ui/view-segmented-control.tsx +1 -1
  125. package/template/docs/data-views-pattern.md +7 -0
  126. package/template/hooks/use-app-theme.ts +1 -1
  127. package/template/hooks/use-coach-mark.ts +1 -1
  128. package/template/hooks/use-location-hash.ts +15 -0
  129. package/template/hooks/use-mobile.ts +1 -1
  130. package/template/hooks/use-mod-key-label.ts +1 -1
  131. package/template/hooks/use-sidebar-reflow-zoom.ts +40 -0
  132. package/template/lib/ask-leo-route-context.ts +25 -57
  133. package/template/lib/coach-mark-registry.ts +13 -13
  134. package/template/lib/command-menu-config.ts +28 -23
  135. package/template/lib/command-menu-search-data.ts +10 -9
  136. package/template/lib/data-list-view-surface.ts +12 -1
  137. package/template/lib/data-list-view.ts +6 -3
  138. package/template/lib/date-filter.ts +1 -1
  139. package/template/lib/mock/dashboard.ts +11 -11
  140. package/template/lib/mock/navigation.tsx +22 -63
  141. package/template/lib/mock/placements-kpi.ts +19 -19
  142. package/template/lib/mock/question-bank-folders.ts +167 -0
  143. package/template/lib/mock/question-bank-header-collaborators.ts +14 -0
  144. package/template/lib/mock/question-bank-inspector.ts +109 -0
  145. package/template/lib/mock/question-bank-kpi.ts +1 -1
  146. package/template/lib/mock/question-bank.ts +80 -0
  147. package/template/lib/question-bank-nav.ts +91 -0
  148. package/template/lib/utils.ts +1 -1
  149. package/template/next.config.mjs +8 -0
  150. package/template/package.json +1 -0
  151. package/template/public/folders/icons8-folder-windows-11.svg +1 -0
  152. package/template/app/(app)/compliance/page.tsx +0 -10
  153. package/template/app/(app)/rotations/page.tsx +0 -15
  154. package/template/app/(app)/sites/all/page.tsx +0 -13
  155. 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: "Pending requests", value: "23", delta: "+5", trend: "up", href: "/data-list#pending" },
9
- { id: "confirmed-placements", label: "Confirmed placements", value: "89", delta: "+12", trend: "up", href: "/data-list#confirmed" },
10
- { id: "pending-reviews", label: "Pending Reviews", value: "8", delta: "-3", trend: "down", href: "/data-list#reviews" },
11
- { id: "available-slots", label: "Available Slots", value: "156", delta: "+24", trend: "up", href: "/data-list#slots" },
12
- { id: "new-applications", label: "New Applications", value: "34", delta: "+7", trend: "up", href: "/data-list#applications"},
13
- { id: "compliance-rate", label: "Compliance Rate", value: "98%", delta: "+2", trend: "up", href: "/data-list#compliance" },
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: "Review Bottleneck",
18
- description: "8 reviews pending with 23 new requests waiting. Clear reviews to maintain placement velocity.",
19
- href: "/reviews",
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: "Student scores",
78
- description: "Your score vs class average on the published scale (reference).",
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: "Placements",
107
+ title: "List hub",
100
108
  url: "/data-list",
101
- icon: <i className="fa-light fa-user-graduate" aria-hidden="true" />,
102
- iconActive: <i className="fa-solid fa-user-graduate" aria-hidden="true" />,
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 = "Documents"
117
+ export const NAV_DOCUMENTS_LABEL = "Resources"
158
118
 
159
119
  export const NAV_DOCUMENTS: NavLinkItem[] = [
160
120
  {
161
- key: "word-assistant",
162
- title: "Word Assistant",
163
- url: "#",
164
- icon: <i className="fa-light fa-file-pen" aria-hidden="true" />,
165
- iconActive: <i className="fa-solid fa-file-pen" aria-hidden="true" />,
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 or ask Leo",
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: "Jordan Rivera",
228
- email: "jordan.rivera@jhmi.edu",
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-jordan-rivera"),
189
+ avatar: stockPortraitUrl("exxat-nav-user-alex-morgan"),
231
190
  }
@@ -1,5 +1,5 @@
1
1
  // ─────────────────────────────────────────────────────────────────────────────
2
- // Placements page — KPI strip + insight (data-list; dashboard view uses row-driven helpers)
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 placement set (table/list/board/dashboard shared state).
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 Placements",
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: "Starting This Week",
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: "Readiness alerts",
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: "Compliance complete",
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: "Pending Reviews",
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 placements moving.`
72
- : "No placements match the current filters.",
73
- href: "/placements/reviews",
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
- * Placements KPI row matches design reference (totals + operational metrics).
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 Placements",
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: "Starting This Week",
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: "Compliance Alerts",
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: "Avg Compliance",
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 still derived from live placement rows for the queue narrative.
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: "Pending Reviews",
127
- description: `${pending} pending, ${inReview} in review. Clear the queue to keep placements moving.`,
128
- href: "/placements/reviews",
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,14 @@
1
+ /**
2
+ * Demo collaborators for Question bank collaboration header (stacked faces).
3
+ */
4
+
5
+ import { stockPortraitUrl } from "@/lib/stock-portrait"
6
+ import type { PageHeaderCollaborator } from "@/components/page-header"
7
+
8
+ export const QUESTION_BANK_HEADER_COLLABORATORS: PageHeaderCollaborator[] = [
9
+ { id: "1", name: "Alex Morgan", imageUrl: stockPortraitUrl("qb-collab-alex"), initials: "AM" },
10
+ { id: "2", name: "Jordan Lee", imageUrl: stockPortraitUrl("qb-collab-jordan"), initials: "JL" },
11
+ { id: "3", name: "Sam Rivera", imageUrl: stockPortraitUrl("qb-collab-sam"), initials: "SR" },
12
+ { id: "4", name: "Taylor Kim", imageUrl: stockPortraitUrl("qb-collab-taylor"), initials: "TK" },
13
+ { id: "5", name: "Riley Patel", imageUrl: stockPortraitUrl("qb-collab-riley"), initials: "RP" },
14
+ ]
@@ -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: "Question bank",
50
+ title: "Folder library",
51
51
  description:
52
52
  review > 0
53
53
  ? `${review} item(s) in review. ${draft} draft(s) not yet published.`