@exxatdesignux/ui 0.2.9 → 0.2.10

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 (125) hide show
  1. package/consumer-extras/cursor-skills/exxat-card-vs-list-rows/SKILL.md +20 -0
  2. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +33 -0
  3. package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
  4. package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
  5. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +31 -5
  6. package/consumer-extras/cursor-skills/exxat-kpi-max-four/SKILL.md +19 -0
  7. package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +27 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +1 -0
  9. package/consumer-extras/patterns/collaboration-access-pattern.md +114 -0
  10. package/consumer-extras/patterns/data-views-pattern.md +12 -4
  11. package/package.json +1 -1
  12. package/src/components/ui/banner.tsx +20 -7
  13. package/src/components/ui/date-picker-field.tsx +3 -3
  14. package/src/components/ui/dropdown-menu.tsx +17 -6
  15. package/src/components/ui/input-group.tsx +1 -1
  16. package/src/components/ui/input.tsx +1 -1
  17. package/src/components/ui/select.tsx +1 -1
  18. package/src/components/ui/separator.tsx +2 -2
  19. package/src/components/ui/sidebar.tsx +31 -3
  20. package/src/components/ui/textarea.tsx +1 -1
  21. package/src/globals.css +0 -1
  22. package/src/index.ts +1 -0
  23. package/src/lib/date-filter.ts +13 -4
  24. package/src/lib/dropdown-menu-surface.ts +13 -0
  25. package/template/.claude/skills/exxat-ds-skill/SKILL.md +27 -9
  26. package/template/.cursor/rules/exxat-data-tables.mdc +1 -0
  27. package/template/AGENTS.md +82 -27
  28. package/template/app/(app)/examples/page.tsx +2 -1
  29. package/template/app/(app)/help/page.tsx +6 -0
  30. package/template/app/(app)/layout.tsx +7 -4
  31. package/template/app/(app)/question-bank/find/page.tsx +12 -0
  32. package/template/app/(app)/question-bank/layout.tsx +46 -0
  33. package/template/app/(app)/question-bank/library/page.tsx +11 -0
  34. package/template/app/(app)/question-bank/list/page.tsx +12 -0
  35. package/template/app/(app)/question-bank/page.tsx +4 -3
  36. package/template/app/globals.css +1 -2
  37. package/template/components/app-sidebar.tsx +51 -13
  38. package/template/components/ask-leo-composer.tsx +173 -45
  39. package/template/components/ask-leo-sidebar.tsx +9 -1
  40. package/template/components/chart-area-interactive.tsx +3 -13
  41. package/template/components/charts-overview.tsx +33 -6
  42. package/template/components/collaboration-access-flow.tsx +144 -0
  43. package/template/components/compliance-page-header.tsx +1 -1
  44. package/template/components/compliance-table.tsx +2 -2
  45. package/template/components/dashboard-tabs.tsx +4 -3
  46. package/template/components/data-list-table-cells.tsx +1 -1
  47. package/template/components/data-list-table.tsx +1 -1
  48. package/template/components/data-table/index.tsx +5 -5
  49. package/template/components/data-table/use-table-state.ts +18 -2
  50. package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
  51. package/template/components/data-view-dashboard-charts-team.tsx +8 -5
  52. package/template/components/data-view-dashboard-charts.tsx +62 -227
  53. package/template/components/dedicated-search-recents.tsx +96 -0
  54. package/template/components/dedicated-search-url-composer.tsx +112 -0
  55. package/template/components/getting-started.tsx +1 -1
  56. package/template/components/hub-tree-panel-view.tsx +10 -26
  57. package/template/components/invite-collaborators-drawer.tsx +453 -0
  58. package/template/components/key-metrics.tsx +54 -8
  59. package/template/components/nav-documents.tsx +1 -1
  60. package/template/components/new-placement-form.tsx +3 -3
  61. package/template/components/page-header.tsx +76 -59
  62. package/template/components/placements-board-view.tsx +3 -3
  63. package/template/components/placements-page-header.tsx +1 -1
  64. package/template/components/placements-table-columns.tsx +3 -2
  65. package/template/components/product-switcher.tsx +0 -1
  66. package/template/components/question-bank-board-view.tsx +35 -47
  67. package/template/components/question-bank-client.tsx +293 -81
  68. package/template/components/question-bank-dashboard-charts.tsx +174 -0
  69. package/template/components/question-bank-favorite-button.tsx +46 -0
  70. package/template/components/question-bank-hub-client.tsx +436 -0
  71. package/template/components/question-bank-list-view.tsx +26 -19
  72. package/template/components/question-bank-new-folder-sheet.tsx +56 -42
  73. package/template/components/question-bank-os-folder-view.tsx +3 -14
  74. package/template/components/question-bank-page-header.tsx +85 -53
  75. package/template/components/question-bank-panel-activator.tsx +3 -4
  76. package/template/components/question-bank-secondary-nav.tsx +523 -65
  77. package/template/components/question-bank-table.tsx +125 -343
  78. package/template/components/secondary-panel.tsx +130 -63
  79. package/template/components/settings-client.tsx +3 -1
  80. package/template/components/sidebar-shell.tsx +2 -0
  81. package/template/components/sites-all-client.tsx +1 -1
  82. package/template/components/sites-table.tsx +1 -1
  83. package/template/components/system-banner-slot.tsx +2 -1
  84. package/template/components/table-properties/drawer.tsx +3 -3
  85. package/template/components/table-properties/sort-card.tsx +1 -1
  86. package/template/components/team-page-header.tsx +1 -1
  87. package/template/components/team-table.tsx +8 -4
  88. package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
  89. package/template/components/templates/dedicated-search-results-template.tsx +19 -0
  90. package/template/components/templates/discovery-hub-template.tsx +273 -0
  91. package/template/components/templates/list-page.tsx +11 -4
  92. package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
  93. package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
  94. package/template/docs/card-vs-rows-pattern.md +36 -0
  95. package/template/docs/collaboration-access-pattern.md +114 -0
  96. package/template/docs/data-views-pattern.md +12 -4
  97. package/template/docs/drawer-vs-dialog-pattern.md +50 -0
  98. package/template/docs/kpi-strip-max-four-pattern.md +29 -0
  99. package/template/docs/kpi-trend-pattern.md +43 -0
  100. package/template/fontawesome-subset.manifest.json +2 -2
  101. package/template/hooks/use-location-hash.ts +14 -8
  102. package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
  103. package/template/lib/ask-leo-route-context.ts +24 -0
  104. package/template/lib/collaborator-access.ts +92 -0
  105. package/template/lib/command-menu-config.ts +8 -1
  106. package/template/lib/command-menu-search-data.ts +11 -8
  107. package/template/lib/data-list-display-options.ts +1 -1
  108. package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
  109. package/template/lib/date-filter.ts +1 -0
  110. package/template/lib/dedicated-search-recents.ts +76 -0
  111. package/template/lib/dedicated-search-url.ts +23 -0
  112. package/template/lib/discovery-hub.ts +15 -0
  113. package/template/lib/list-status-badges.ts +1 -21
  114. package/template/lib/mock/navigation.tsx +4 -2
  115. package/template/lib/mock/placements.ts +9 -9
  116. package/template/lib/mock/question-bank-folders.ts +7 -0
  117. package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
  118. package/template/lib/mock/question-bank-inspector.ts +1 -2
  119. package/template/lib/mock/question-bank-kpi.ts +38 -26
  120. package/template/lib/mock/question-bank.ts +43 -16
  121. package/template/lib/question-bank-dedicated-search.ts +19 -0
  122. package/template/lib/question-bank-hub-search.ts +90 -0
  123. package/template/lib/question-bank-nav.ts +322 -6
  124. package/template/lib/question-bank-recent-searches.ts +22 -0
  125. package/template/package.json +0 -1
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Namespaced recent-query storage for dedicated search routes (localStorage + sync event).
3
+ * Hub code creates one controller per surface (namespace) and passes it into
4
+ * {@link DedicatedSearchRecents} / {@link DedicatedSearchUrlComposer}.
5
+ */
6
+
7
+ const MAX_RECENTS = 12
8
+
9
+ function parseStored(raw: string | null): string[] {
10
+ if (!raw) return []
11
+ try {
12
+ const parsed = JSON.parse(raw) as unknown
13
+ if (!Array.isArray(parsed)) return []
14
+ return parsed
15
+ .filter((x): x is string => typeof x === "string" && x.trim().length > 0)
16
+ .map(s => s.trim())
17
+ .slice(0, MAX_RECENTS)
18
+ } catch {
19
+ return []
20
+ }
21
+ }
22
+
23
+ export interface DedicatedSearchRecentsController {
24
+ /** Pass to `addEventListener` / `removeEventListener` (CustomEvent, no detail). */
25
+ readonly eventName: string
26
+ read: () => string[]
27
+ record: (query: string) => void
28
+ clear: () => void
29
+ }
30
+
31
+ export type DedicatedSearchRecentsLegacyKeys = {
32
+ storageKey: string
33
+ eventName: string
34
+ }
35
+
36
+ /**
37
+ * @param namespace — Stable id when not using `legacy` (storage key + event name derive from it).
38
+ * @param legacy — Optional stable keys for an existing shipped surface (avoid resetting users’ saved recents).
39
+ */
40
+ export function createDedicatedSearchRecentsController(
41
+ namespace: string,
42
+ legacy?: DedicatedSearchRecentsLegacyKeys,
43
+ ): DedicatedSearchRecentsController {
44
+ const storageKey = legacy?.storageKey ?? `exxat-ds.dedicated-search.recents.${namespace}.v1`
45
+ const eventName = legacy?.eventName ?? `exxat-dedicated-search-recents-${namespace}`
46
+
47
+ const read = (): string[] => {
48
+ if (typeof window === "undefined") return []
49
+ return parseStored(window.localStorage.getItem(storageKey))
50
+ }
51
+
52
+ const record = (query: string): void => {
53
+ const q = query.trim()
54
+ if (!q || typeof window === "undefined") return
55
+ const prev = read()
56
+ const deduped = [q, ...prev.filter(x => x.toLowerCase() !== q.toLowerCase())].slice(0, MAX_RECENTS)
57
+ try {
58
+ window.localStorage.setItem(storageKey, JSON.stringify(deduped))
59
+ } catch {
60
+ /* ignore quota / private mode */
61
+ }
62
+ window.dispatchEvent(new CustomEvent(eventName))
63
+ }
64
+
65
+ const clear = (): void => {
66
+ if (typeof window === "undefined") return
67
+ try {
68
+ window.localStorage.removeItem(storageKey)
69
+ } catch {
70
+ /* ignore */
71
+ }
72
+ window.dispatchEvent(new CustomEvent(eventName))
73
+ }
74
+
75
+ return { eventName, read, record, clear }
76
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * URL helpers for dedicated search surfaces — hubs pass domain-specific patchers.
3
+ */
4
+
5
+ export type DedicatedSearchParamsPatch = (
6
+ /** Current query string snapshot (e.g. from `useSearchParams` serialization). */
7
+ searchParamsKey: string,
8
+ /** Trimmed query text; empty string means “clear primary search param”. */
9
+ submittedText: string,
10
+ ) => URLSearchParams
11
+
12
+ /** Default: single `q` param, replaces or deletes only `q`. */
13
+ export function patchDedicatedSearchQueryParam(
14
+ searchParamsKey: string,
15
+ submittedText: string,
16
+ paramName = "q",
17
+ ): URLSearchParams {
18
+ const next = new URLSearchParams(searchParamsKey)
19
+ const t = submittedText.trim()
20
+ if (t) next.set(paramName, t)
21
+ else next.delete(paramName)
22
+ return next
23
+ }
@@ -0,0 +1,15 @@
1
+ export type DiscoveryHubSearchItem = {
2
+ id: string
3
+ label: string
4
+ keywords?: string
5
+ icon?: string
6
+ href?: string
7
+ askLeoPrompt?: string
8
+ }
9
+
10
+ export type DiscoveryHubSearchGroup = {
11
+ id: string
12
+ heading: string
13
+ items: DiscoveryHubSearchItem[]
14
+ searchOnly?: boolean
15
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Shared status chip labels, tint classes, and FA icon names for product list hubs
3
- * (Placements, Team, Compliance, Question bank — table, list, board), plus related chips
3
+ * (Placements, Team, Compliance — table, list, board), plus related chips
4
4
  * (dashboard **task priority**, placement **readiness** on the detail drawer).
5
5
  *
6
6
  * Labels use **sentence / title case** (e.g. "Due soon", "Under Review"). Do **not** add **`uppercase`**.
@@ -14,7 +14,6 @@
14
14
  */
15
15
 
16
16
  import type { ComplianceStatus } from "@/lib/mock/compliance"
17
- import type { QuestionBankStatus } from "@/lib/mock/question-bank"
18
17
  import type { Status as PlacementStatus } from "@/lib/mock/placements"
19
18
  import type { TeamMember } from "@/lib/mock/team"
20
19
 
@@ -147,22 +146,3 @@ export const COMPLIANCE_STATUS_ICON: Record<ComplianceStatus, string> = {
147
146
  pending: "fa-hourglass-half",
148
147
  }
149
148
 
150
- // ─── Question bank ────────────────────────────────────────────────────────
151
-
152
- export const QUESTION_BANK_STATUS_LABEL: Record<QuestionBankStatus, string> = {
153
- published: "Published",
154
- draft: "Draft",
155
- in_review: "In review",
156
- }
157
-
158
- export const QUESTION_BANK_STATUS_BADGE_CLASS: Record<QuestionBankStatus, string> = {
159
- published: LIST_HUB_STATUS_TINT_SUCCESS,
160
- draft: LIST_HUB_STATUS_TINT_NEUTRAL,
161
- in_review: LIST_HUB_STATUS_TINT_WARNING,
162
- }
163
-
164
- export const QUESTION_BANK_STATUS_ICON: Record<QuestionBankStatus, string> = {
165
- published: "fa-circle-check",
166
- draft: "fa-pen-field",
167
- in_review: "fa-user-magnifying-glass",
168
- }
@@ -120,14 +120,16 @@ export const NAV_DOCUMENTS: NavLinkItem[] = [
120
120
  {
121
121
  key: "tokens",
122
122
  title: "Tokens & themes",
123
- url: "/settings",
123
+ /** Same page as Settings — disambiguate active state via `#appearance` (see `isNavActive`). */
124
+ url: "/settings#appearance",
124
125
  icon: <i className="fa-light fa-palette" aria-hidden="true" />,
125
126
  iconActive: <i className="fa-solid fa-palette" aria-hidden="true" />,
126
127
  },
127
128
  {
128
129
  key: "more",
129
130
  title: "More",
130
- url: "/help",
131
+ /** Same page as Get Help — disambiguate via `#more`. */
132
+ url: "/help#more",
131
133
  icon: <i className="fa-light fa-ellipsis" aria-hidden="true" />,
132
134
  iconActive: <i className="fa-solid fa-ellipsis" aria-hidden="true" />,
133
135
  },
@@ -46,7 +46,7 @@ export const ALL_PLACEMENTS: Placement[] = [
46
46
  id: 1, student: "Sarah Johnson", email: "s.johnson@college.edu", initials: "SJ", program: "Nursing", site: "City Medical Center",
47
47
  siteAddress: "1400 N Lake Shore Dr, Chicago, IL", status: "confirmed", start: "03/15/2026", duration: "12 wks", supervisor: "Dr. Patel",
48
48
  placementPhase: "ongoing", internship: "Med-Surg Clinical I", specialization: "Adult Health", compliance: "Complete", daysUntilStart: 0,
49
- readiness: "Ready", progressWeeksDone: 5, progressWeeksTotal: 12, endDate: "06/07/2026", lastCheckin: "Mar 22, 2026",
49
+ readiness: "Ready", progressWeeksDone: 5, progressWeeksTotal: 12, endDate: "06/07/2026", lastCheckin: "03/22/2026",
50
50
  completionDate: "—", finalStatus: "—", rating: 0, suggestedToHire: "—", isNew: true,
51
51
  },
52
52
  {
@@ -67,7 +67,7 @@ export const ALL_PLACEMENTS: Placement[] = [
67
67
  id: 4, student: "James Williams", email: "j.williams@college.edu", initials: "JW", program: "Nursing", site: "Sunrise Hospital",
68
68
  siteAddress: "5775 Wayzata Blvd, Minneapolis, MN", status: "confirmed", start: "04/07/2026", duration: "12 wks", supervisor: "Dr. Torres",
69
69
  placementPhase: "ongoing", internship: "ICU Practicum", specialization: "Critical Care", compliance: "Complete", daysUntilStart: 0,
70
- readiness: "Ready", progressWeeksDone: 2, progressWeeksTotal: 12, endDate: "06/30/2026", lastCheckin: "Mar 21, 2026",
70
+ readiness: "Ready", progressWeeksDone: 2, progressWeeksTotal: 12, endDate: "06/30/2026", lastCheckin: "03/21/2026",
71
71
  completionDate: "—", finalStatus: "—", rating: 0, suggestedToHire: "—",
72
72
  },
73
73
  {
@@ -88,7 +88,7 @@ export const ALL_PLACEMENTS: Placement[] = [
88
88
  id: 7, student: "Priya Sharma", email: "p.sharma@college.edu", initials: "PS", program: "Nursing", site: "Harbor Medical",
89
89
  siteAddress: "1200 W Harrison St, Chicago, IL", status: "confirmed", start: "03/22/2026", duration: "12 wks", supervisor: "Dr. Patel",
90
90
  placementPhase: "ongoing", internship: "Pediatric Nursing", specialization: "Pediatrics", compliance: "Complete", daysUntilStart: 0,
91
- readiness: "Ready", progressWeeksDone: 7, progressWeeksTotal: 12, endDate: "06/14/2026", lastCheckin: "Mar 23, 2026",
91
+ readiness: "Ready", progressWeeksDone: 7, progressWeeksTotal: 12, endDate: "06/14/2026", lastCheckin: "03/23/2026",
92
92
  completionDate: "—", finalStatus: "—", rating: 0, suggestedToHire: "—", isNew: true,
93
93
  },
94
94
  {
@@ -102,7 +102,7 @@ export const ALL_PLACEMENTS: Placement[] = [
102
102
  id: 9, student: "Lena Fischer", email: "l.fischer@school.edu", initials: "LF", program: "Nursing", site: "Westside Clinic",
103
103
  siteAddress: "2525 S Michigan Ave, Chicago, IL", status: "confirmed", start: "03/18/2026", duration: "12 wks", supervisor: "Dr. Santos",
104
104
  placementPhase: "ongoing", internship: "Primary Care RN", specialization: "Family Practice", compliance: "Complete", daysUntilStart: 0,
105
- readiness: "Ready", progressWeeksDone: 8, progressWeeksTotal: 12, endDate: "06/10/2026", lastCheckin: "Mar 24, 2026",
105
+ readiness: "Ready", progressWeeksDone: 8, progressWeeksTotal: 12, endDate: "06/10/2026", lastCheckin: "03/24/2026",
106
106
  completionDate: "—", finalStatus: "—", rating: 0, suggestedToHire: "—",
107
107
  },
108
108
  {
@@ -116,7 +116,7 @@ export const ALL_PLACEMENTS: Placement[] = [
116
116
  id: 11, student: "Nina Patel", email: "n.patel@university.edu", initials: "NP", program: "Social Work", site: "Hope Community Ctr",
117
117
  siteAddress: "3517 W Arthington St, Chicago, IL", status: "confirmed", start: "03/25/2026", duration: "10 wks", supervisor: "Ms. Torres",
118
118
  placementPhase: "ongoing", internship: "School Social Work", specialization: "Youth Services", compliance: "Complete", daysUntilStart: 0,
119
- readiness: "Ready", progressWeeksDone: 4, progressWeeksTotal: 10, endDate: "06/03/2026", lastCheckin: "Mar 19, 2026",
119
+ readiness: "Ready", progressWeeksDone: 4, progressWeeksTotal: 10, endDate: "06/03/2026", lastCheckin: "03/19/2026",
120
120
  completionDate: "—", finalStatus: "—", rating: 0, suggestedToHire: "—",
121
121
  },
122
122
  {
@@ -130,28 +130,28 @@ export const ALL_PLACEMENTS: Placement[] = [
130
130
  id: 13, student: "Alex Morgan", email: "a.morgan@college.edu", initials: "AM", program: "Nursing", site: "City Medical Center",
131
131
  siteAddress: "1400 N Lake Shore Dr, Chicago, IL", status: "completed", start: "01/06/2026", duration: "12 wks", supervisor: "Dr. Patel",
132
132
  placementPhase: "completed", internship: "Med-Surg Clinical II", specialization: "Adult Health", compliance: "Complete", daysUntilStart: 0,
133
- readiness: "Ready", progressWeeksDone: 12, progressWeeksTotal: 12, endDate: "03/30/2026", lastCheckin: "Mar 28, 2026",
133
+ readiness: "Ready", progressWeeksDone: 12, progressWeeksTotal: 12, endDate: "03/30/2026", lastCheckin: "03/28/2026",
134
134
  completionDate: "03/30/2026", finalStatus: "Passed", rating: 4.8, suggestedToHire: "Yes",
135
135
  },
136
136
  {
137
137
  id: 14, student: "Jordan Lee", email: "j.lee@university.edu", initials: "JL", program: "Physical Therapy", site: "Metro Rehab",
138
138
  siteAddress: "250 E Superior St, Chicago, IL", status: "completed", start: "11/04/2025", duration: "8 wks", supervisor: "Dr. Kim",
139
139
  placementPhase: "completed", internship: "Inpatient PT", specialization: "Acute Care", compliance: "Complete", daysUntilStart: 0,
140
- readiness: "Ready", progressWeeksDone: 8, progressWeeksTotal: 8, endDate: "12/30/2025", lastCheckin: "Dec 28, 2025",
140
+ readiness: "Ready", progressWeeksDone: 8, progressWeeksTotal: 8, endDate: "12/30/2025", lastCheckin: "12/28/2025",
141
141
  completionDate: "12/30/2025", finalStatus: "Passed", rating: 4.2, suggestedToHire: "Yes",
142
142
  },
143
143
  {
144
144
  id: 15, student: "Sam Rivera", email: "s.rivera@school.edu", initials: "SR", program: "Occupational Therapy", site: "Bay Area Health",
145
145
  siteAddress: "3100 Telegraph Ave, Oakland, CA", status: "completed", start: "10/01/2025", duration: "10 wks", supervisor: "Dr. Nguyen",
146
146
  placementPhase: "completed", internship: "Hand Therapy OT", specialization: "Hand Therapy", compliance: "Complete", daysUntilStart: 0,
147
- readiness: "Ready", progressWeeksDone: 10, progressWeeksTotal: 10, endDate: "12/10/2025", lastCheckin: "Dec 08, 2025",
147
+ readiness: "Ready", progressWeeksDone: 10, progressWeeksTotal: 10, endDate: "12/10/2025", lastCheckin: "12/08/2025",
148
148
  completionDate: "12/10/2025", finalStatus: "Incomplete", rating: 3.5, suggestedToHire: "No",
149
149
  },
150
150
  {
151
151
  id: 16, student: "Taylor Brooks", email: "t.brooks@college.edu", initials: "TB", program: "Nursing", site: "Sunrise Hospital",
152
152
  siteAddress: "5775 Wayzata Blvd, Minneapolis, MN", status: "completed", start: "09/02/2025", duration: "12 wks", supervisor: "Dr. Torres",
153
153
  placementPhase: "completed", internship: "Labor & Delivery", specialization: "Women's Health", compliance: "Complete", daysUntilStart: 0,
154
- readiness: "Ready", progressWeeksDone: 12, progressWeeksTotal: 12, endDate: "11/25/2025", lastCheckin: "Nov 22, 2025",
154
+ readiness: "Ready", progressWeeksDone: 12, progressWeeksTotal: 12, endDate: "11/25/2025", lastCheckin: "11/22/2025",
155
155
  completionDate: "11/25/2025", finalStatus: "Passed", rating: 5, suggestedToHire: "Yes",
156
156
  },
157
157
  ]
@@ -102,6 +102,13 @@ export const QUESTION_BANK_FOLDER_ICON_OPTIONS: readonly string[] = [
102
102
  ] as const
103
103
 
104
104
  export const DEFAULT_QUESTION_BANK_FOLDERS: QuestionBankFolder[] = [
105
+ {
106
+ id: "fld-favorites",
107
+ name: "Favorites",
108
+ parentId: null,
109
+ icon: "fa-star",
110
+ colorKey: "warning",
111
+ },
105
112
  {
106
113
  id: "fld-clinical",
107
114
  name: "Clinical",
@@ -6,9 +6,49 @@ import { stockPortraitUrl } from "@/lib/stock-portrait"
6
6
  import type { PageHeaderCollaborator } from "@/components/page-header"
7
7
 
8
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" },
9
+ {
10
+ id: "1",
11
+ name: "Alex Morgan",
12
+ email: "alex.morgan@example.com",
13
+ imageUrl: stockPortraitUrl("qb-collab-alex"),
14
+ initials: "AM",
15
+ access: "owner",
16
+ roles: ["Director", "Faculty"],
17
+ },
18
+ {
19
+ id: "2",
20
+ name: "Jordan Lee",
21
+ email: "jordan.lee@example.com",
22
+ imageUrl: stockPortraitUrl("qb-collab-jordan"),
23
+ initials: "JL",
24
+ access: "editor",
25
+ roles: ["Program coordinator"],
26
+ },
27
+ {
28
+ id: "3",
29
+ name: "Sam Rivera",
30
+ email: "sam.rivera@example.com",
31
+ imageUrl: stockPortraitUrl("qb-collab-sam"),
32
+ initials: "SR",
33
+ access: "editor",
34
+ roles: ["Faculty"],
35
+ },
36
+ {
37
+ id: "4",
38
+ name: "Taylor Kim",
39
+ email: "taylor.kim@example.com",
40
+ imageUrl: stockPortraitUrl("qb-collab-taylor"),
41
+ initials: "TK",
42
+ access: "commenter",
43
+ roles: ["Faculty"],
44
+ },
45
+ {
46
+ id: "5",
47
+ name: "Riley Patel",
48
+ email: "riley.patel@example.com",
49
+ imageUrl: stockPortraitUrl("qb-collab-riley"),
50
+ initials: "RP",
51
+ access: "viewer",
52
+ roles: ["Program coordinator"],
53
+ },
14
54
  ]
@@ -15,8 +15,7 @@ export const QUESTION_TYPE_ABBREV: Record<QuestionBankType, string> = {
15
15
  export function deriveQuestionItemCode(q: QuestionBankItem): string {
16
16
  const raw = q.itemCode?.trim()
17
17
  if (raw) return raw
18
- const n = q.id.replace(/\D/g, "") || "0"
19
- return `QB-${String(n).padStart(3, "0")}`
18
+ return q.questionId
20
19
  }
21
20
 
22
21
  export function deriveBloomLevel(q: QuestionBankItem): string {
@@ -1,10 +1,21 @@
1
1
  import type { MetricInsight, MetricItem } from "@/components/key-metrics"
2
- import type { QuestionBankItem } from "@/lib/mock/question-bank"
2
+ import type { QuestionBankItem, QuestionBankType } from "@/lib/mock/question-bank"
3
+
4
+ /** Point-biserial below this mock threshold counts as a psychometric review flag. */
5
+ const PBI_REVIEW_THRESHOLD = 0.2
6
+
7
+ const TYPE_LABEL: Record<QuestionBankType, string> = {
8
+ multiple_choice: "Multiple choice",
9
+ true_false: "True / false",
10
+ short_answer: "Short answer",
11
+ }
3
12
 
4
13
  export function questionBankKpiMetrics(rows: QuestionBankItem[]): MetricItem[] {
5
- const published = rows.filter(r => r.status === "published").length
6
- const draft = rows.filter(r => r.status === "draft").length
7
- const review = rows.filter(r => r.status === "in_review").length
14
+ const mcq = rows.filter(r => r.type === "multiple_choice").length
15
+ const tf = rows.filter(r => r.type === "true_false").length
16
+ const sa = rows.filter(r => r.type === "short_answer").length
17
+ const writtenTypes = tf + sa
18
+ const lowPbiFlags = rows.filter(r => r.pbi != null && r.pbi < PBI_REVIEW_THRESHOLD).length
8
19
 
9
20
  return [
10
21
  {
@@ -17,45 +28,46 @@ export function questionBankKpiMetrics(rows: QuestionBankItem[]): MetricItem[] {
17
28
  metricVariant: "hero",
18
29
  },
19
30
  {
20
- id: "published",
21
- label: "Published",
22
- value: published,
31
+ id: "mcq",
32
+ label: TYPE_LABEL.multiple_choice,
33
+ value: mcq,
23
34
  delta: "—",
24
35
  trend: "neutral",
25
36
  href: "#",
26
37
  },
27
38
  {
28
- id: "review",
29
- label: "In review",
30
- value: review,
31
- delta: review > 0 ? "!" : "—",
32
- trend: review > 0 ? "up" : "neutral",
39
+ id: "written",
40
+ label: "True / false & short answer",
41
+ value: writtenTypes,
42
+ delta: "—",
43
+ trend: "neutral",
33
44
  href: "#",
34
45
  },
35
46
  {
36
- id: "draft",
37
- label: "Drafts",
38
- value: draft,
39
- delta: "—",
40
- trend: "neutral",
47
+ id: "pbi-flags",
48
+ label: "Low PBI (review)",
49
+ value: lowPbiFlags,
50
+ delta: lowPbiFlags >= 2 ? "+1" : "—",
51
+ trend: lowPbiFlags >= 2 ? "up" : "neutral",
52
+ trendPolarity: "lower_is_better",
41
53
  href: "#",
42
54
  },
43
55
  ]
44
56
  }
45
57
 
46
58
  export function questionBankKpiInsight(rows: QuestionBankItem[]): MetricInsight {
47
- const review = rows.filter(r => r.status === "in_review").length
48
- const draft = rows.filter(r => r.status === "draft").length
59
+ const hard = rows.filter(r => r.difficulty === "hard").length
60
+ const topics = new Set(rows.map(r => r.topic)).size
49
61
  return {
50
62
  title: "Folder library",
51
63
  description:
52
- review > 0
53
- ? `${review} item(s) in review. ${draft} draft(s) not yet published.`
54
- : draft > 0
55
- ? `${draft} draft(s) ready to finalize or send for review.`
56
- : "All items are published or in review with no backlog.",
57
- href: "/question-bank",
58
- severity: review > 2 ? "warning" : "info",
64
+ rows.length === 0
65
+ ? "Add questions to populate metrics and charts."
66
+ : hard > 3
67
+ ? `${hard} hard items in this view balance with easier items where learners need quick wins.`
68
+ : `${rows.length} question${rows.length === 1 ? "" : "s"} across ${topics} topic${topics === 1 ? "" : "s"} in the filtered set.`,
69
+ href: "/question-bank/library",
70
+ severity: hard > 3 ? "warning" : "info",
59
71
  actionLabel: "Ask Leo",
60
72
  }
61
73
  }
@@ -2,7 +2,6 @@
2
2
  * Mock question bank items — replace with API in production.
3
3
  */
4
4
 
5
- export type QuestionBankStatus = "published" | "draft" | "in_review"
6
5
  export type QuestionBankType = "multiple_choice" | "true_false" | "short_answer"
7
6
  export type QuestionBankDifficulty = "easy" | "medium" | "hard"
8
7
 
@@ -17,13 +16,16 @@ export type QuestionBankBloomLevel =
17
16
 
18
17
  export interface QuestionBankItem extends Record<string, unknown> {
19
18
  id: string
19
+ /** Stable human-facing identifier (catalog, search, citations). */
20
+ questionId: string
20
21
  /** Short preview / stem */
21
22
  stem: string
22
23
  topic: string
23
24
  type: QuestionBankType
24
25
  difficulty: QuestionBankDifficulty
25
- status: QuestionBankStatus
26
26
  author: string
27
+ /** Work email for the primary author (demo; optional when API omits). */
28
+ authorEmail?: string
27
29
  updatedAt: string
28
30
  /** Folder tree id (`lib/mock/question-bank-folders.ts`). */
29
31
  folderId: string
@@ -54,19 +56,27 @@ export interface QuestionBankItem extends Record<string, unknown> {
54
56
  avgScoreCorrectPct?: number
55
57
  /** Where / when the item was last used on an exam. */
56
58
  lastUsedLabel?: string
59
+ /** Starred outside the Favorites folder (list landing demo). */
60
+ isStarred?: boolean
61
+ }
62
+
63
+ /** New mock rows — assign a unique `questionId` when creating client-side. */
64
+ export function newQuestionBankQuestionId(): string {
65
+ return `QB-NEW-${Date.now().toString(36).toUpperCase()}`
57
66
  }
58
67
 
59
68
  export const QUESTION_BANK_ITEMS: QuestionBankItem[] = [
60
69
  {
61
70
  id: "q1",
71
+ questionId: "QB-ANA-001",
62
72
  stem: "Which nerve roots contribute to the brachial plexus?",
63
73
  topic: "Anatomy",
64
74
  type: "multiple_choice",
65
75
  difficulty: "medium",
66
- status: "published",
67
76
  author: "Dr. Chen",
77
+ authorEmail: "mei.chen@demo.exxat.io",
68
78
  updatedAt: "2026-03-28",
69
- folderId: "fld-science",
79
+ folderId: "fld-favorites",
70
80
  itemCode: "QB-ANA-001",
71
81
  bloomLevel: "Apply",
72
82
  tags: ["Brachial plexus", "Peripheral nerves", "Spine"],
@@ -88,36 +98,45 @@ export const QUESTION_BANK_ITEMS: QuestionBankItem[] = [
88
98
  },
89
99
  {
90
100
  id: "q2",
101
+ questionId: "QB-CNL-002",
91
102
  stem: "Document baseline vitals before administering contrast.",
92
103
  topic: "Clinical skills",
93
104
  type: "true_false",
94
105
  difficulty: "easy",
95
- status: "published",
96
106
  author: "Jordan Lee",
107
+ authorEmail: "jordan.lee@demo.exxat.io",
97
108
  updatedAt: "2026-03-27",
98
109
  folderId: "fld-skills-lab",
110
+ isStarred: true,
111
+ pbi: 0.55,
99
112
  },
100
113
  {
101
114
  id: "q3",
115
+ questionId: "QB-NEU-003",
102
116
  stem: "List three red flags for cauda equina syndrome.",
103
117
  topic: "Neurology",
104
118
  type: "short_answer",
105
119
  difficulty: "hard",
106
- status: "in_review",
107
120
  author: "Alex Rivera",
121
+ authorEmail: "alex.rivera@demo.exxat.io",
108
122
  updatedAt: "2026-03-26",
109
123
  folderId: "fld-science",
124
+ isStarred: true,
125
+ pbi: 0.14,
110
126
  },
111
127
  {
112
128
  id: "q4",
129
+ questionId: "QB-ETH-004",
113
130
  stem: "HIPAA permits disclosure to family without consent when…",
114
131
  topic: "Ethics & law",
115
132
  type: "multiple_choice",
116
133
  difficulty: "medium",
117
- status: "draft",
118
134
  author: "Sam Patel",
135
+ authorEmail: "sam.patel@demo.exxat.io",
119
136
  updatedAt: "2026-03-25",
120
137
  folderId: "fld-ethics",
138
+ isStarred: true,
139
+ pbi: 0.19,
121
140
  options: [
122
141
  { text: "Patient is incapacitated and disclosure is in their best interest", isCorrect: true },
123
142
  { text: "Family member requests the information", isCorrect: false },
@@ -127,23 +146,25 @@ export const QUESTION_BANK_ITEMS: QuestionBankItem[] = [
127
146
  },
128
147
  {
129
148
  id: "q5",
149
+ questionId: "QB-ASM-005",
130
150
  stem: "Calculate BMI given height and weight (metric).",
131
151
  topic: "Assessment",
132
152
  type: "short_answer",
133
153
  difficulty: "easy",
134
- status: "published",
135
154
  author: "Dr. Chen",
155
+ authorEmail: "mei.chen@demo.exxat.io",
136
156
  updatedAt: "2026-03-24",
137
- folderId: "fld-clinical",
157
+ folderId: "fld-favorites",
138
158
  },
139
159
  {
140
160
  id: "q6",
161
+ questionId: "QB-ICO-006",
141
162
  stem: "Sterile field must be prepared before which step?",
142
163
  topic: "Infection control",
143
164
  type: "multiple_choice",
144
165
  difficulty: "medium",
145
- status: "published",
146
166
  author: "Morgan Lee",
167
+ authorEmail: "morgan.lee@demo.exxat.io",
147
168
  updatedAt: "2026-03-23",
148
169
  folderId: "fld-ops",
149
170
  options: [
@@ -155,67 +176,73 @@ export const QUESTION_BANK_ITEMS: QuestionBankItem[] = [
155
176
  },
156
177
  {
157
178
  id: "q7",
179
+ questionId: "QB-DOC-007",
158
180
  stem: "SOAP note: subjective section documents patient-reported data only.",
159
181
  topic: "Documentation",
160
182
  type: "true_false",
161
183
  difficulty: "easy",
162
- status: "draft",
163
184
  author: "Casey Nguyen",
185
+ authorEmail: "casey.nguyen@demo.exxat.io",
164
186
  updatedAt: "2026-03-22",
165
187
  folderId: "fld-clinical",
166
188
  },
167
189
  {
168
190
  id: "q8",
191
+ questionId: "QB-RAD-008",
169
192
  stem: "Contrast MRI safety screening includes renal function when…",
170
193
  topic: "Radiology",
171
194
  type: "multiple_choice",
172
195
  difficulty: "hard",
173
- status: "in_review",
174
196
  author: "Riley Johnson",
197
+ authorEmail: "riley.johnson@demo.exxat.io",
175
198
  updatedAt: "2026-03-21",
176
199
  folderId: "fld-science",
177
200
  },
178
201
  {
179
202
  id: "q9",
203
+ questionId: "QB-COM-009",
180
204
  stem: "Therapeutic communication: reflect feelings before offering solutions.",
181
205
  topic: "Communication",
182
206
  type: "true_false",
183
207
  difficulty: "medium",
184
- status: "published",
185
208
  author: "Quinn Martinez",
209
+ authorEmail: "quinn.martinez@demo.exxat.io",
186
210
  updatedAt: "2026-03-20",
187
211
  folderId: "fld-clinical",
188
212
  },
189
213
  {
190
214
  id: "q10",
215
+ questionId: "QB-PHA-010",
191
216
  stem: "Pediatric dose calculation uses body surface area when…",
192
217
  topic: "Pharmacology",
193
218
  type: "short_answer",
194
219
  difficulty: "hard",
195
- status: "published",
196
220
  author: "Dr. Chen",
221
+ authorEmail: "mei.chen@demo.exxat.io",
197
222
  updatedAt: "2026-03-19",
198
223
  folderId: "fld-science",
199
224
  },
200
225
  {
201
226
  id: "q11",
227
+ questionId: "QB-SAF-011",
202
228
  stem: "Fall risk assessment should be repeated after medication changes.",
203
229
  topic: "Safety",
204
230
  type: "true_false",
205
231
  difficulty: "easy",
206
- status: "draft",
207
232
  author: "Taylor Brooks",
233
+ authorEmail: "taylor.brooks@demo.exxat.io",
208
234
  updatedAt: "2026-03-18",
209
235
  folderId: "fld-ops",
210
236
  },
211
237
  {
212
238
  id: "q12",
239
+ questionId: "QB-ICO-012",
213
240
  stem: "Describe hand hygiene moments (WHO five moments).",
214
241
  topic: "Infection control",
215
242
  type: "short_answer",
216
243
  difficulty: "medium",
217
- status: "in_review",
218
244
  author: "Jordan Lee",
245
+ authorEmail: "jordan.lee@demo.exxat.io",
219
246
  updatedAt: "2026-03-17",
220
247
  folderId: "fld-ops",
221
248
  },