@utopia-studio-design/design-system-cli 0.7.1 → 0.8.0

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 (247) hide show
  1. package/bin/utopia-ds-mcp.mjs +18 -5
  2. package/bin/utopia-ds.mjs +16 -3
  3. package/data/blocks/semantic-pattern-parts/AuthParts.tsx +48 -0
  4. package/data/blocks/semantic-pattern-parts/ChoiceCards.tsx +7 -0
  5. package/data/blocks/semantic-pattern-parts/ListFilters.tsx +9 -0
  6. package/data/blocks/semantic-pattern-parts/OutcomeNotice.tsx +7 -0
  7. package/data/blocks/semantic-pattern-parts/PatternParts.tsx +7 -0
  8. package/data/blocks/semantic-pattern-parts/ProductParts.tsx +38 -0
  9. package/data/blocks/semantic-pattern-parts/ProgressSteps.tsx +6 -0
  10. package/data/blocks/semantic-pattern-parts/ReviewList.tsx +7 -0
  11. package/data/blocks/semantic-pattern-parts/ValidatedField.tsx +8 -0
  12. package/data/blocks/semantic-pattern-parts/WorkflowParts.tsx +339 -0
  13. package/data/blocks/semantic-pattern-parts/auth-parts.css +27 -0
  14. package/data/blocks/semantic-pattern-parts/pattern-parts.css +25 -0
  15. package/data/blocks/semantic-pattern-parts/product-parts.css +25 -0
  16. package/data/blocks/semantic-pattern-parts/types.ts +3 -0
  17. package/data/blocks/semantic-pattern-parts/workflow-parts.css +118 -0
  18. package/data/blocks/ux-pattern-examples/AccessExamples.tsx +46 -0
  19. package/data/blocks/ux-pattern-examples/AccountExamples.tsx +133 -0
  20. package/data/blocks/ux-pattern-examples/OnboardingExample.tsx +15 -0
  21. package/data/blocks/ux-pattern-examples/ProductExamples.tsx +79 -0
  22. package/data/blocks/ux-pattern-examples/ReviewExample.tsx +14 -0
  23. package/data/blocks/ux-pattern-examples/SignupExample.tsx +22 -0
  24. package/data/blocks/ux-pattern-examples/WorkflowExample.tsx +1761 -0
  25. package/data/blocks/ux-pattern-examples/WorkflowExamples.tsx +190 -0
  26. package/data/blocks/ux-pattern-examples/copy.ts +6 -0
  27. package/data/blocks/ux-pattern-examples/ux-examples.css +18 -0
  28. package/data/blocks/ux-pattern-examples/workflow-copy.ts +76 -0
  29. package/data/blocks/ux-pattern-examples/workflow-definitions.ts +1291 -0
  30. package/data/docs/ceramic-release-2026-09-06.md +2 -0
  31. package/data/docs/create-a-brandbook.md +79 -0
  32. package/data/docs/dextrum-document-family.md +47 -0
  33. package/data/docs/guide.md +2 -0
  34. package/data/docs/presentation-asset-repository.md +5 -0
  35. package/data/docs/ui-parts-qa.md +172 -0
  36. package/data/docs/ui-parts.md +105 -0
  37. package/data/docs/ux-pattern-adaptation-qa.md +38 -0
  38. package/data/docs/ux-pattern-adaptation.md +41 -0
  39. package/data/docs/ux-pattern-migration.md +111 -0
  40. package/data/docs/ux-pattern-release-2026-09-13.md +26 -0
  41. package/data/docs/ux-source-evidence/account-auth.json +41 -0
  42. package/data/docs/ux-source-evidence/account-forgot-password.json +36 -0
  43. package/data/docs/ux-source-evidence/account-invite.json +63 -0
  44. package/data/docs/ux-source-evidence/account-locked.json +63 -0
  45. package/data/docs/ux-source-evidence/account-login.json +28 -0
  46. package/data/docs/ux-source-evidence/account-mfa.json +62 -0
  47. package/data/docs/ux-source-evidence/account-onboarding-credentials.json +52 -0
  48. package/data/docs/ux-source-evidence/account-onboarding-legal.json +56 -0
  49. package/data/docs/ux-source-evidence/account-onboarding-orientation.json +66 -0
  50. package/data/docs/ux-source-evidence/account-onboarding-profile.json +50 -0
  51. package/data/docs/ux-source-evidence/account-onboarding.json +69 -0
  52. package/data/docs/ux-source-evidence/account-reset-password.json +27 -0
  53. package/data/docs/ux-source-evidence/account-session-expired.json +62 -0
  54. package/data/docs/ux-source-evidence/account-signup.json +33 -0
  55. package/data/docs/ux-source-evidence/account-verify-email.json +34 -0
  56. package/data/docs/ux-source-evidence/account-welcome.json +41 -0
  57. package/data/docs/ux-source-evidence/admin-analytics.json +54 -0
  58. package/data/docs/ux-source-evidence/admin-audit.json +31 -0
  59. package/data/docs/ux-source-evidence/admin-broadcasts-new.json +36 -0
  60. package/data/docs/ux-source-evidence/admin-broadcasts.json +41 -0
  61. package/data/docs/ux-source-evidence/admin-closures.json +52 -0
  62. package/data/docs/ux-source-evidence/admin-devices.json +51 -0
  63. package/data/docs/ux-source-evidence/admin-escalations.json +35 -0
  64. package/data/docs/ux-source-evidence/admin-incidents.json +54 -0
  65. package/data/docs/ux-source-evidence/admin-loading.json +17 -0
  66. package/data/docs/ux-source-evidence/admin-onboarding-detail.json +80 -0
  67. package/data/docs/ux-source-evidence/admin-onboarding.json +62 -0
  68. package/data/docs/ux-source-evidence/admin-payments.json +45 -0
  69. package/data/docs/ux-source-evidence/admin-providers.json +38 -0
  70. package/data/docs/ux-source-evidence/admin-settings.json +18 -0
  71. package/data/docs/ux-source-evidence/admin-subscriptions-detail-allocate.json +42 -0
  72. package/data/docs/ux-source-evidence/admin-subscriptions-detail.json +48 -0
  73. package/data/docs/ux-source-evidence/admin-subscriptions-new.json +43 -0
  74. package/data/docs/ux-source-evidence/admin-subscriptions.json +41 -0
  75. package/data/docs/ux-source-evidence/admin-transactions.json +41 -0
  76. package/data/docs/ux-source-evidence/error.json +30 -0
  77. package/data/docs/ux-source-evidence/loading.json +19 -0
  78. package/data/docs/ux-source-evidence/marketing-blog.json +48 -0
  79. package/data/docs/ux-source-evidence/marketing-contact.json +36 -0
  80. package/data/docs/ux-source-evidence/marketing.json +64 -0
  81. package/data/docs/ux-source-evidence/not-found.json +28 -0
  82. package/data/docs/ux-source-evidence/portal-loading.json +17 -0
  83. package/data/docs/ux-source-evidence/portal-mentor-messages.json +31 -0
  84. package/data/docs/ux-source-evidence/portal-mentor-profile.json +23 -0
  85. package/data/docs/ux-source-evidence/portal-mentor-requests-detail.json +54 -0
  86. package/data/docs/ux-source-evidence/portal-mentor-sessions-detail-assess.json +42 -0
  87. package/data/docs/ux-source-evidence/portal-mentor-settings.json +18 -0
  88. package/data/docs/ux-source-evidence/portal-mentor.json +38 -0
  89. package/data/docs/ux-source-evidence/portal-sessions-detail-live.json +48 -0
  90. package/data/docs/ux-source-evidence/portal-sponsor-billing.json +28 -0
  91. package/data/docs/ux-source-evidence/portal-sponsor-profile.json +22 -0
  92. package/data/docs/ux-source-evidence/portal-sponsor-sessions.json +28 -0
  93. package/data/docs/ux-source-evidence/portal-sponsor-settings.json +18 -0
  94. package/data/docs/ux-source-evidence/portal-sponsor-trainees.json +36 -0
  95. package/data/docs/ux-source-evidence/portal-sponsor.json +52 -0
  96. package/data/docs/ux-source-evidence/portal-trainee-competency.json +42 -0
  97. package/data/docs/ux-source-evidence/portal-trainee-credits.json +40 -0
  98. package/data/docs/ux-source-evidence/portal-trainee-mentors.json +48 -0
  99. package/data/docs/ux-source-evidence/portal-trainee-messages.json +32 -0
  100. package/data/docs/ux-source-evidence/portal-trainee-notifications.json +35 -0
  101. package/data/docs/ux-source-evidence/portal-trainee-profile.json +26 -0
  102. package/data/docs/ux-source-evidence/portal-trainee-request.json +54 -0
  103. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +36 -0
  104. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +42 -0
  105. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail.json +55 -0
  106. package/data/docs/ux-source-evidence/portal-trainee-sessions.json +36 -0
  107. package/data/docs/ux-source-evidence/portal-trainee-settings-notifications.json +38 -0
  108. package/data/docs/ux-source-evidence/portal-trainee-settings.json +18 -0
  109. package/data/docs/ux-source-evidence/portal-trainee.json +44 -0
  110. package/data/docs/ux-source-evidence/portal-tv-detail.json +39 -0
  111. package/data/docs/ux-source-evidence/portal-tv.json +44 -0
  112. package/data/docs/ux-source-evidence/shared-admin-admin-shell.json +35 -0
  113. package/data/docs/ux-source-evidence/shared-auth-auth-primitives.json +105 -0
  114. package/data/docs/ux-source-evidence/shared-auth-auth-shell.json +25 -0
  115. package/data/docs/ux-source-evidence/shared-auth-onboarding-primitives.json +52 -0
  116. package/data/docs/ux-source-evidence/shared-auth-onboarding-shell.json +28 -0
  117. package/data/docs/ux-source-evidence/shared-auth-status-indicator.json +41 -0
  118. package/data/docs/ux-source-evidence/shared-portal-empty-state.json +24 -0
  119. package/data/docs/ux-source-evidence/shared-portal-loading-skeleton.json +27 -0
  120. package/data/docs/ux-source-evidence/shared-portal-notifications-dropdown.json +39 -0
  121. package/data/docs/ux-source-evidence/shared-portal-portal-shell.json +36 -0
  122. package/data/docs/ux-source-evidence/shared-portal-profile-form.json +32 -0
  123. package/data/docs/ux-source-evidence/shared-portal-settings-view.json +81 -0
  124. package/data/docs/ux-source-evidence/shared-portal-usage-sparkline.json +18 -0
  125. package/data/docs/ux-source-evidence/shared-portal-user-menu.json +34 -0
  126. package/data/manifests/blocks.json +230 -21
  127. package/data/manifests/brandbook-skill.json +33 -0
  128. package/data/manifests/catalog.json +1 -0
  129. package/data/manifests/patterns.json +358 -0
  130. package/data/manifests/theme-dextrum.json +132 -21
  131. package/data/manifests/themes.json +253 -39
  132. package/data/manifests/ui-patterns.json +1029 -0
  133. package/data/manifests/ux-examples.json +2177 -0
  134. package/data/manifests/ux-patterns.json +1135 -0
  135. package/data/manifests/ux-source-inventory.json +4669 -0
  136. package/data/ui-parts/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  137. package/data/ui-parts/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
  138. package/data/ui-parts/blocks/semantic-pattern-parts/AuthParts.tsx +48 -0
  139. package/data/ui-parts/blocks/semantic-pattern-parts/ChoiceCards.tsx +7 -0
  140. package/data/ui-parts/blocks/semantic-pattern-parts/ListFilters.tsx +9 -0
  141. package/data/ui-parts/blocks/semantic-pattern-parts/OutcomeNotice.tsx +7 -0
  142. package/data/ui-parts/blocks/semantic-pattern-parts/ProductParts.tsx +38 -0
  143. package/data/ui-parts/blocks/semantic-pattern-parts/ProgressSteps.tsx +6 -0
  144. package/data/ui-parts/blocks/semantic-pattern-parts/ReviewList.tsx +7 -0
  145. package/data/ui-parts/blocks/semantic-pattern-parts/ValidatedField.tsx +8 -0
  146. package/data/ui-parts/blocks/semantic-pattern-parts/WorkflowParts.tsx +339 -0
  147. package/data/ui-parts/blocks/semantic-pattern-parts/auth-parts.css +27 -0
  148. package/data/ui-parts/blocks/semantic-pattern-parts/pattern-parts.css +25 -0
  149. package/data/ui-parts/blocks/semantic-pattern-parts/product-parts.css +25 -0
  150. package/data/ui-parts/blocks/semantic-pattern-parts/types.ts +3 -0
  151. package/data/ui-parts/blocks/semantic-pattern-parts/workflow-parts.css +118 -0
  152. package/data/ui-parts/blocks/ux-pattern-examples/AccessExamples.tsx +46 -0
  153. package/data/ui-parts/blocks/ux-pattern-examples/AccountExamples.tsx +133 -0
  154. package/data/ui-parts/blocks/ux-pattern-examples/ProductExamples.tsx +79 -0
  155. package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExample.tsx +1761 -0
  156. package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExamples.tsx +190 -0
  157. package/data/ui-parts/blocks/ux-pattern-examples/copy.ts +6 -0
  158. package/data/ui-parts/blocks/ux-pattern-examples/ux-examples.css +18 -0
  159. package/data/ui-parts/blocks/ux-pattern-examples/workflow-copy.ts +76 -0
  160. package/data/ui-parts/blocks/ux-pattern-examples/workflow-definitions.ts +1291 -0
  161. package/data/ux-source-evidence/account-auth.json +41 -0
  162. package/data/ux-source-evidence/account-forgot-password.json +36 -0
  163. package/data/ux-source-evidence/account-invite.json +63 -0
  164. package/data/ux-source-evidence/account-locked.json +63 -0
  165. package/data/ux-source-evidence/account-login.json +28 -0
  166. package/data/ux-source-evidence/account-mfa.json +62 -0
  167. package/data/ux-source-evidence/account-onboarding-credentials.json +52 -0
  168. package/data/ux-source-evidence/account-onboarding-legal.json +56 -0
  169. package/data/ux-source-evidence/account-onboarding-orientation.json +66 -0
  170. package/data/ux-source-evidence/account-onboarding-profile.json +50 -0
  171. package/data/ux-source-evidence/account-onboarding.json +69 -0
  172. package/data/ux-source-evidence/account-reset-password.json +27 -0
  173. package/data/ux-source-evidence/account-session-expired.json +62 -0
  174. package/data/ux-source-evidence/account-signup.json +33 -0
  175. package/data/ux-source-evidence/account-verify-email.json +34 -0
  176. package/data/ux-source-evidence/account-welcome.json +41 -0
  177. package/data/ux-source-evidence/admin-analytics.json +54 -0
  178. package/data/ux-source-evidence/admin-audit.json +31 -0
  179. package/data/ux-source-evidence/admin-broadcasts-new.json +36 -0
  180. package/data/ux-source-evidence/admin-broadcasts.json +41 -0
  181. package/data/ux-source-evidence/admin-closures.json +52 -0
  182. package/data/ux-source-evidence/admin-devices.json +51 -0
  183. package/data/ux-source-evidence/admin-escalations.json +35 -0
  184. package/data/ux-source-evidence/admin-incidents.json +54 -0
  185. package/data/ux-source-evidence/admin-loading.json +17 -0
  186. package/data/ux-source-evidence/admin-onboarding-detail.json +80 -0
  187. package/data/ux-source-evidence/admin-onboarding.json +62 -0
  188. package/data/ux-source-evidence/admin-payments.json +45 -0
  189. package/data/ux-source-evidence/admin-providers.json +38 -0
  190. package/data/ux-source-evidence/admin-settings.json +18 -0
  191. package/data/ux-source-evidence/admin-subscriptions-detail-allocate.json +42 -0
  192. package/data/ux-source-evidence/admin-subscriptions-detail.json +48 -0
  193. package/data/ux-source-evidence/admin-subscriptions-new.json +43 -0
  194. package/data/ux-source-evidence/admin-subscriptions.json +41 -0
  195. package/data/ux-source-evidence/admin-transactions.json +41 -0
  196. package/data/ux-source-evidence/error.json +30 -0
  197. package/data/ux-source-evidence/loading.json +19 -0
  198. package/data/ux-source-evidence/marketing-blog.json +48 -0
  199. package/data/ux-source-evidence/marketing-contact.json +36 -0
  200. package/data/ux-source-evidence/marketing.json +64 -0
  201. package/data/ux-source-evidence/not-found.json +28 -0
  202. package/data/ux-source-evidence/portal-loading.json +17 -0
  203. package/data/ux-source-evidence/portal-mentor-messages.json +31 -0
  204. package/data/ux-source-evidence/portal-mentor-profile.json +23 -0
  205. package/data/ux-source-evidence/portal-mentor-requests-detail.json +54 -0
  206. package/data/ux-source-evidence/portal-mentor-sessions-detail-assess.json +42 -0
  207. package/data/ux-source-evidence/portal-mentor-settings.json +18 -0
  208. package/data/ux-source-evidence/portal-mentor.json +38 -0
  209. package/data/ux-source-evidence/portal-sessions-detail-live.json +48 -0
  210. package/data/ux-source-evidence/portal-sponsor-billing.json +28 -0
  211. package/data/ux-source-evidence/portal-sponsor-profile.json +22 -0
  212. package/data/ux-source-evidence/portal-sponsor-sessions.json +28 -0
  213. package/data/ux-source-evidence/portal-sponsor-settings.json +18 -0
  214. package/data/ux-source-evidence/portal-sponsor-trainees.json +36 -0
  215. package/data/ux-source-evidence/portal-sponsor.json +52 -0
  216. package/data/ux-source-evidence/portal-trainee-competency.json +42 -0
  217. package/data/ux-source-evidence/portal-trainee-credits.json +40 -0
  218. package/data/ux-source-evidence/portal-trainee-mentors.json +48 -0
  219. package/data/ux-source-evidence/portal-trainee-messages.json +32 -0
  220. package/data/ux-source-evidence/portal-trainee-notifications.json +35 -0
  221. package/data/ux-source-evidence/portal-trainee-profile.json +26 -0
  222. package/data/ux-source-evidence/portal-trainee-request.json +54 -0
  223. package/data/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +36 -0
  224. package/data/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +42 -0
  225. package/data/ux-source-evidence/portal-trainee-sessions-detail.json +55 -0
  226. package/data/ux-source-evidence/portal-trainee-sessions.json +36 -0
  227. package/data/ux-source-evidence/portal-trainee-settings-notifications.json +38 -0
  228. package/data/ux-source-evidence/portal-trainee-settings.json +18 -0
  229. package/data/ux-source-evidence/portal-trainee.json +44 -0
  230. package/data/ux-source-evidence/portal-tv-detail.json +39 -0
  231. package/data/ux-source-evidence/portal-tv.json +44 -0
  232. package/data/ux-source-evidence/shared-admin-admin-shell.json +35 -0
  233. package/data/ux-source-evidence/shared-auth-auth-primitives.json +105 -0
  234. package/data/ux-source-evidence/shared-auth-auth-shell.json +25 -0
  235. package/data/ux-source-evidence/shared-auth-onboarding-primitives.json +52 -0
  236. package/data/ux-source-evidence/shared-auth-onboarding-shell.json +28 -0
  237. package/data/ux-source-evidence/shared-auth-status-indicator.json +41 -0
  238. package/data/ux-source-evidence/shared-portal-empty-state.json +24 -0
  239. package/data/ux-source-evidence/shared-portal-loading-skeleton.json +27 -0
  240. package/data/ux-source-evidence/shared-portal-notifications-dropdown.json +39 -0
  241. package/data/ux-source-evidence/shared-portal-portal-shell.json +36 -0
  242. package/data/ux-source-evidence/shared-portal-profile-form.json +32 -0
  243. package/data/ux-source-evidence/shared-portal-settings-view.json +81 -0
  244. package/data/ux-source-evidence/shared-portal-usage-sparkline.json +18 -0
  245. package/data/ux-source-evidence/shared-portal-user-menu.json +34 -0
  246. package/lib/api.mjs +228 -4
  247. package/package.json +2 -2
@@ -0,0 +1,48 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/sessions/[id]/live/page.tsx",
4
+ "sha256": "5a47bfd48fb70f0173c95576ecefbdf8bf3d77ca2119e9cbe7e2dfbec5d4106d",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "lucide-react",
9
+ "@/components/ui/button",
10
+ "@/components/ui/badge",
11
+ "@/components/ui/card",
12
+ "@/components/ui/textarea",
13
+ "@/components/ui/separator",
14
+ "@/components/auth/auth-primitives"
15
+ ],
16
+ "tokens": [
17
+ "--amber-500",
18
+ "--color-bg-brand-muted",
19
+ "--color-bg-subtle",
20
+ "--color-status-info-bg",
21
+ "--color-status-info-text",
22
+ "--color-status-success-bg",
23
+ "--color-status-success-text",
24
+ "--color-status-warning-bg",
25
+ "--color-status-warning-text",
26
+ "--color-text-brand",
27
+ "--color-text-secondary",
28
+ "--gray-0",
29
+ "--gray-50",
30
+ "--gray-900",
31
+ "--gray-950",
32
+ "--green-500",
33
+ "--red-500"
34
+ ],
35
+ "fragments": [
36
+ {
37
+ "id": "LiveSessionPage",
38
+ "line": 37,
39
+ "code": "export default function LiveSessionPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = use(params);\n\n const [participants, setParticipants] = useState<Participant[]>([\n { id: \"p1\", role: \"trainee\", name: \"Dr. Yuki Tanaka\", initials: \"YT\", micOn: true, videoOn: true, hasFeed: true },\n { id: \"p2\", role: \"mentor\", name: \"Dr. Hiroshi Yamamoto\", initials: \"HY\", micOn: true, videoOn: true },\n { id: \"p3\", role: \"industry\", name: \"Olympus Medical (Rep)\", initials: \"OM\", org: \"Olympus\", micOn: false, videoOn: false },\n { id: \"p4\", role: \"admin\", name: \"Sarah K.\", initials: \"SK\", micOn: false, videoOn: false },\n { id: \"p5\", role: \"tech\", name: \"Surgease Support\", initials: \"SS\", micOn: false, videoOn: false },\n ]);\n\n const me = participants[1]; // playing the mentor role\n\n const [elapsed, setElapsed] = useState(0); // seconds\n const [safetyOpen, setSafetyOpen] = useState(false);\n const [safetyNote, setSafetyNote] = useState(\"\");\n const [safetyEvents, setSafetyEvents] = useState<{ at: number; note: string }[]>([]);\n const [connectionLost, setConnectionLost] = useState(false);\n\n useEffect(() => {\n const t = setInterval(() => setElapsed((s) => s + 1), 1000);\n return () => clearInterval(t);\n }, []);\n\n const toggleMic = (id: string) => setParticipants((ps) => ps.map((p) => p.id === id ? { ...p, micOn: !p.micOn } : p));\n\n const trainee = participants.find((p) => p.role === \"trainee\")!;\n\n // 2-hour warning at 1:45 (105 minutes = 6300s). For demo, use 30s.\n const approachingLimit = elapsed > 30;\n\n const mins = Math.floor(elapsed / 60);\n const secs = elapsed % 60;\n const timeStr = `${String(mins).padStart(2, \"0\")}:${String(secs).padStart(2, \"0\")}`;\n\n return (\n <div className=\"min-h-screen flex flex-col\" style={{ background: \"var(--gray-950)\", color: \"var(--gray-50)\" }}>\n {/* Top bar */}\n <header className=\"h-14 px-6 flex items-center justify-between shrink-0 border-b\" style={{ background: \"color-mix(in srgb, var(--gray-950) 80%, transparent)\", borderColor: \"color-mix(in srgb, var(--gray-0) 8%, transparent)\", backdropFilter: \"blur(8px)\" }}>\n <div className=\"flex items-center gap-4\">\n <div className=\"flex items-center gap-2\">\n <span className=\"w-2 h-2 rounded-full animate-pulse\" style={{ background: \"var(--red-500)\" }} />\n <span className=\"text-[11px] font-semibold uppercase tracking-wider\">REC</span>\n </div>\n <Separator orientation=\"vertical\" className=\"h-5\" style={{ background: \"color-mix(in srgb, var(--gray-0) 15%, transparent)\" }} />\n <div className=\"text-[12px]\" style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }}>\n <span className=\"text-[11px] uppercase tracking-wider\" style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }}>Session</span>\n <span className=\"ml-2 font-mono\">{id}</span>\n </div>\n <Separator orientation=\"vertical\" className=\"h-5\" style={{ background: \"color-mix(in srgb, var(--gray-0) 15%, transparent)\" }} />\n <div className=\"flex items-center gap-1.5 text-[13px] tabular-nums font-semibold\" style={{ color: approachingLimit ? \"var(--amber-500)\" : \"var(--gray-50)\" }}>\n <Activity size={13} strokeWidth={2} />\n {timeStr}\n {approachingLimit && <Badge className=\"text-[10px] ml-1\" style={{ background: \"var(--amber-500)\", color: \"var(--gray-950)\" }}>Limit approaching</Badge>}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2 text-[11px]\" style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }}>\n <Wifi size={12} strokeWidth={1.8} style={{ color: \"var(--green-500)\" }} />\n <span>1080p · 28ms latency · stable</span>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Settings\" style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }}><Settings size={14} /></Button>\n <Link href={`/portal/mentor/sessions/${id}/assess`}>\n <Button variant=\"destructive\" size=\"sm\">\n <PhoneOff size={12} /> End session\n </Button>\n </Link>\n </div>\n </header>\n\n {/* Main grid */}\n <main className=\"flex-1 flex p-4 gap-4 overflow-hidden\">\n {/* Video grid */}\n <div className=\"flex-1 grid grid-rows-[1fr_140px] gap-3 min-w-0\">\n {/* Primary feed (trainee) */}\n <div className=\"rounded-xl relative overflow-hidden\" style={{ background: \"color-mix(in srgb, var(--gray-0) 4%, transparent)\", border: \"1px solid color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n {/* \"Live procedural video\" mock */}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <div className=\"text-center\">\n <div className=\"text-[60px] mb-2\" style={{ opacity: 0.2 }}>📷</div>\n <p className=\"text-[12px]\" style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }}>\n Live procedural feed from {trainee.name}'s teleproctoring device\n </p>\n </div>\n </div>\n {/* Overlay info */}\n <div className=\"absolute top-3 left-3 flex items-center gap-2\">\n <Badge className=\"text-[10px]\" style={{ background: \"color-mix(in srgb, var(--gray-950) 70%, transparent)\", color: \"var(--gray-50)\", backdropFilter: \"blur(4px)\" }}>\n {trainee.name} · Operating\n </Badge>\n </div>\n <div className=\"absolute top-3 right-3 flex items-center gap-2\">\n <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Maximize\" style={{ background: \"color-mix(in srgb, var(--gray-950) 50%, transparent)\", color: \"var(--gray-50)\" }}>\n <Maximize2 size={13} />\n </Button>\n </div>\n\n {/* Connectivity loss overlay */}\n {connectionLost && (\n <div className=\"absolute inset-0 flex items-center justify-center\" style={{ background: \"color-mix(in srgb, var(--red-500) 80%, transparent)\", backdropFilter: \"blur(2px)\" }}>\n <div className=\"text-center max-w-md p-6\">\n <AlertTriangle size={48} strokeWidth={1.5} className=\"mx-auto mb-3\" />\n <h2 className=\"text-xl font-bold mb-2\">FAIL-SAFE ACTIVATED</h2>\n <p className=\"text-[14px] mb-4\">Connection lost. Trainee should revert to local supervision immediately. Reconnection in progress…</p>\n <Button variant=\"default\" onClick={() => setConnectionLost(false)} style={{ background: \"var(--gray-0)\", color: \"var(--red-500)\" }}>\n Mark resolved\n </Button>\n </div>\n </div>\n )}\n </div>\n\n {/* Participant strip */}\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-3\">\n {participants.filter((p) => !p.hasFeed).map((p) => (\n <ParticipantTile key={p.id} p={p} onToggleMic={() => toggleMic(p.id)} isMe={p.id === me.id} />\n ))}\n </div>\n </div>\n\n {/* Sidebar */}\n <aside className=\"w-80 flex flex-col gap-3 shrink-0\">\n {/* Participants */}\n <Card className=\"flex-1 overflow-hidden\" style={{ background: \"color-mix(in srgb, var(--gray-0) 4%, transparent)\", border: \"1px solid color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <div className=\"px-4 py-3 flex items-center justify-between border-b\" style={{ borderColor: \"color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <h3 className=\"text-[12px] font-semibold uppercase tracking-wider flex items-center gap-2\">\n <Users size={12} /> Participants ({participants.length})\n </h3>\n </div>\n <div className=\"p-2 space-y-1\">\n {participants.map((p) => (\n <div key={p.id} className=\"px-2 py-2 rounded-md flex items-center gap-2.5\">\n <div className=\"w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: ROLE_META[p.role].bg, color: ROLE_META[p.role].color }}>\n {p.initials}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[12px] truncate\" style={{ color: \"var(--gray-50)\" }}>{p.name}</div>\n <div className=\"text-[10px] truncate\" style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }}>{ROLE_META[p.role].label}</div>\n </div>\n <div className=\"flex items-center gap-1\">\n {p.micOn ? <Mic size={11} style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }} /> : <MicOff size={11} style={{ color: \"color-mix(in srgb, var(--gray-0) 30%, transparent)\" }} />}\n {p.videoOn ? <VideoIcon size={11} style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }} /> : <VideoOff size={11} style={{ color: \"color-mix(in srgb, var(--gray-0) 30%, transparent)\" }} />}\n </div>\n </div>\n ))}\n </div>\n </Card>\n\n {/* Safety events */}\n <Card className=\"overflow-hidden\" style={{ background: \"color-mix(in srgb, var(--gray-0) 4%, transparent)\", border: \"1px solid color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <div className=\"px-4 py-3 flex items-center justify-between border-b\" style={{ borderColor: \"color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <h3 className=\"text-[12px] font-semibold uppercase tracking-wider\">Safety events</h3>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setSafetyOpen(true)} style={{ color: \"var(--amber-500)\" }}>\n <Flag size={11} /> Flag\n </Button>\n </div>\n <div className=\"p-3\">\n {safetyEvents.length === 0 ? (\n <p className=\"text-[11px]\" style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }}>No events flagged.</p>\n ) : (\n <ul className=\"space-y-2 text-[11px]\">\n {safetyEvents.map((e, i) => (\n <li key={i} className=\"flex items-start gap-2\">\n <Flag size={10} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--amber-500)\" }} />\n <div>\n <div className=\"font-mono tabular-nums\" style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }}>{Math.floor(e.at / 60)}:{String(e.at % 60).padStart(2, \"0\")}</div>\n <div style={{ color: \"var(--gray-50)\" }}>{e.note}</div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n </Card>\n\n {/* Connectivity test toggle (demo) */}\n <Button variant=\"outline\" size=\"sm\" onClick={() => setConnectionLost(true)} style={{ borderColor: \"color-mix(in srgb, var(--gray-0) 15%, transparent)\", color: \"var(--gray-50)\", background: \"transparent\" }}>\n Simulate connection loss\n </Button>\n </aside>\n </main>\n\n {/* Bottom controls */}\n <footer className=\"h-16 px-6 flex items-center justify-center gap-2 shrink-0 border-t\" style={{ background: \"color-mix(in srgb, var(--gray-950) 80%, transparent)\", borderColor: \"color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <Button\n variant=\"ghost\"\n size=\"icon-lg\"\n onClick={() => toggleMic(me.id)}\n aria-label=\"Mute\"\n style={{ background: me.micOn ? \"color-mix(in srgb, var(--gray-0) 8%, transparent)\" : \"var(--red-500)\", color: \"var(--gray-50)\" }}\n >\n {me.micOn ? <Mic size={16} /> : <MicOff size={16} />}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"icon-lg\"\n aria-label=\"Camera\"\n style={{ background: me.videoOn ? \"color-mix(in srgb, var(--gray-0) 8%, transparent)\" : \"var(--red-500)\", color: \"var(--gray-50)\" }}\n >\n {me.videoOn ? <VideoIcon size={16} /> : <VideoOff size={16} />}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"icon-lg\"\n onClick={() => setSafetyOpen(true)}\n aria-label=\"Flag safety event\"\n style={{ background: \"color-mix(in srgb, var(--gray-0) 8%, transparent)\", color: \"var(--amber-500)\" }}\n >\n <Flag size={16} />\n </Button>\n </footer>\n\n {/* Safety event modal */}\n {safetyOpen && (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\" style={{ background: \"color-mix(in srgb, var(--gray-950) 80%, transparent)\", backdropFilter: \"blur(4px)\" }}>\n <div className=\"w-full max-w-md rounded-2xl p-6\" style={{ background: \"var(--gray-900)\", border: \"1px solid color-mix(in srgb, var(--gray-0) 10%, transparent)\" }}>\n <div className=\"flex items-start gap-3 mb-4\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center shrink-0\" style={{ background: \"color-mix(in srgb, var(--amber-500) 20%, transparent)\", color: \"var(--amber-500)\" }}>\n <Flag size={18} strokeWidth={1.8} />\n </div>\n <div>\n <h3 className=\"text-base font-semibold mb-1\" style={{ color: \"var(--gray-50)\" }}>Flag safety event</h3>\n <p className=\"text-[12px]\" style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }}>\n Clinical responsibility remains with the operating surgeon. Provide guidance as appropriate.\n </p>\n </div>\n </div>\n <Textarea\n value={safetyNote}\n onChange={(e) => setSafetyNote(e.target.value.slice(0, 200))}\n placeholder=\"Brief description (max 200 chars)…\"\n rows={3}\n />\n <p className=\"text-[10px] mt-1 mb-4\" style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }}>\n {safetyNote.length} / 200\n </p>\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"outline\" onClick={() => { setSafetyOpen(false); setSafetyNote(\"\"); }}>Cancel</Button>\n <Button variant=\"brand\" disabled={!safetyNote.trim()} onClick={() => {\n setSafetyEvents([...safetyEvents, { at: elapsed, note: safetyNote }]);\n setSafetyOpen(false); setSafetyNote(\"\");\n }}>\n <Flag size={11} /> Log event\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n );\n}"
40
+ },
41
+ {
42
+ "id": "ParticipantTile",
43
+ "line": 291,
44
+ "code": "function ParticipantTile({ p, onToggleMic, isMe }: { p: Participant; onToggleMic: () => void; isMe: boolean }) {\n const meta = ROLE_META[p.role];\n return (\n <div className=\"rounded-xl relative overflow-hidden flex items-center justify-center\" style={{ background: \"color-mix(in srgb, var(--gray-0) 5%, transparent)\", border: \"1px solid color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <div className=\"w-12 h-12 rounded-full flex items-center justify-center text-[14px] font-semibold\" style={{ background: meta.bg, color: meta.color }}>\n {p.initials}\n </div>\n <div className=\"absolute bottom-2 left-2 right-2 flex items-center justify-between\">\n <div className=\"min-w-0 flex-1\">\n <div className=\"text-[11px] font-medium truncate\" style={{ color: \"var(--gray-50)\" }}>\n {p.name.split(\" \").slice(0, 2).join(\" \")}{isMe && \" (You)\"}\n </div>\n <div className=\"text-[9px] truncate\" style={{ color: meta.color }}>{meta.label}</div>\n </div>\n <div className=\"flex items-center gap-1 shrink-0\">\n {p.micOn\n ? <Mic size={10} style={{ color: \"var(--gray-50)\" }} />\n : <MicOff size={10} style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }} />\n }\n </div>\n </div>\n </div>\n );\n}"
45
+ }
46
+ ],
47
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { use, useState, useEffect } from \"react\";\nimport {\n Mic, MicOff, Video as VideoIcon, VideoOff, PhoneOff, AlertTriangle,\n Wifi, Activity, Users, FileText, Flag, Settings, Maximize2,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card } from \"@/components/ui/card\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { InlineAlert } from \"@/components/auth/auth-primitives\";\n\ntype ParticipantRole = \"mentor\" | \"trainee\" | \"industry\" | \"admin\" | \"tech\";\n\ninterface Participant {\n id: string;\n role: ParticipantRole;\n name: string;\n initials: string;\n org?: string;\n micOn: boolean;\n videoOn: boolean;\n hasFeed?: boolean; // for trainee primary feed\n}\n\nconst ROLE_META: Record<ParticipantRole, { label: string; color: string; bg: string }> = {\n mentor: { label: \"Mentor\", color: \"var(--color-text-brand)\", bg: \"var(--color-bg-brand-muted)\" },\n trainee: { label: \"Operating surgeon\", color: \"var(--color-status-success-text)\", bg: \"var(--color-status-success-bg)\" },\n industry: { label: \"Industry partner\", color: \"var(--color-status-info-text)\", bg: \"var(--color-status-info-bg)\" },\n admin: { label: \"Mentix admin\", color: \"var(--color-text-secondary)\", bg: \"var(--color-bg-subtle)\" },\n tech: { label: \"Tech support\", color: \"var(--color-status-warning-text)\", bg: \"var(--color-status-warning-bg)\" },\n};\n\nexport default function LiveSessionPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = use(params);\n\n const [participants, setParticipants] = useState<Participant[]>([\n { id: \"p1\", role: \"trainee\", name: \"Dr. Yuki Tanaka\", initials: \"YT\", micOn: true, videoOn: true, hasFeed: true },\n { id: \"p2\", role: \"mentor\", name: \"Dr. Hiroshi Yamamoto\", initials: \"HY\", micOn: true, videoOn: true },\n { id: \"p3\", role: \"industry\", name: \"Olympus Medical (Rep)\", initials: \"OM\", org: \"Olympus\", micOn: false, videoOn: false },\n { id: \"p4\", role: \"admin\", name: \"Sarah K.\", initials: \"SK\", micOn: false, videoOn: false },\n { id: \"p5\", role: \"tech\", name: \"Surgease Support\", initials: \"SS\", micOn: false, videoOn: false },\n ]);\n\n const me = participants[1]; // playing the mentor role\n\n const [elapsed, setElapsed] = useState(0); // seconds\n const [safetyOpen, setSafetyOpen] = useState(false);\n const [safetyNote, setSafetyNote] = useState(\"\");\n const [safetyEvents, setSafetyEvents] = useState<{ at: number; note: string }[]>([]);\n const [connectionLost, setConnectionLost] = useState(false);\n\n useEffect(() => {\n const t = setInterval(() => setElapsed((s) => s + 1), 1000);\n return () => clearInterval(t);\n }, []);\n\n const toggleMic = (id: string) => setParticipants((ps) => ps.map((p) => p.id === id ? { ...p, micOn: !p.micOn } : p));\n\n const trainee = participants.find((p) => p.role === \"trainee\")!;\n\n // 2-hour warning at 1:45 (105 minutes = 6300s). For demo, use 30s.\n const approachingLimit = elapsed > 30;\n\n const mins = Math.floor(elapsed / 60);\n const secs = elapsed % 60;\n const timeStr = `${String(mins).padStart(2, \"0\")}:${String(secs).padStart(2, \"0\")}`;\n\n return (\n <div className=\"min-h-screen flex flex-col\" style={{ background: \"var(--gray-950)\", color: \"var(--gray-50)\" }}>\n {/* Top bar */}\n <header className=\"h-14 px-6 flex items-center justify-between shrink-0 border-b\" style={{ background: \"color-mix(in srgb, var(--gray-950) 80%, transparent)\", borderColor: \"color-mix(in srgb, var(--gray-0) 8%, transparent)\", backdropFilter: \"blur(8px)\" }}>\n <div className=\"flex items-center gap-4\">\n <div className=\"flex items-center gap-2\">\n <span className=\"w-2 h-2 rounded-full animate-pulse\" style={{ background: \"var(--red-500)\" }} />\n <span className=\"text-[11px] font-semibold uppercase tracking-wider\">REC</span>\n </div>\n <Separator orientation=\"vertical\" className=\"h-5\" style={{ background: \"color-mix(in srgb, var(--gray-0) 15%, transparent)\" }} />\n <div className=\"text-[12px]\" style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }}>\n <span className=\"text-[11px] uppercase tracking-wider\" style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }}>Session</span>\n <span className=\"ml-2 font-mono\">{id}</span>\n </div>\n <Separator orientation=\"vertical\" className=\"h-5\" style={{ background: \"color-mix(in srgb, var(--gray-0) 15%, transparent)\" }} />\n <div className=\"flex items-center gap-1.5 text-[13px] tabular-nums font-semibold\" style={{ color: approachingLimit ? \"var(--amber-500)\" : \"var(--gray-50)\" }}>\n <Activity size={13} strokeWidth={2} />\n {timeStr}\n {approachingLimit && <Badge className=\"text-[10px] ml-1\" style={{ background: \"var(--amber-500)\", color: \"var(--gray-950)\" }}>Limit approaching</Badge>}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2 text-[11px]\" style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }}>\n <Wifi size={12} strokeWidth={1.8} style={{ color: \"var(--green-500)\" }} />\n <span>1080p · 28ms latency · stable</span>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Settings\" style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }}><Settings size={14} /></Button>\n <Link href={`/portal/mentor/sessions/${id}/assess`}>\n <Button variant=\"destructive\" size=\"sm\">\n <PhoneOff size={12} /> End session\n </Button>\n </Link>\n </div>\n </header>\n\n {/* Main grid */}\n <main className=\"flex-1 flex p-4 gap-4 overflow-hidden\">\n {/* Video grid */}\n <div className=\"flex-1 grid grid-rows-[1fr_140px] gap-3 min-w-0\">\n {/* Primary feed (trainee) */}\n <div className=\"rounded-xl relative overflow-hidden\" style={{ background: \"color-mix(in srgb, var(--gray-0) 4%, transparent)\", border: \"1px solid color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n {/* \"Live procedural video\" mock */}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <div className=\"text-center\">\n <div className=\"text-[60px] mb-2\" style={{ opacity: 0.2 }}>📷</div>\n <p className=\"text-[12px]\" style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }}>\n Live procedural feed from {trainee.name}'s teleproctoring device\n </p>\n </div>\n </div>\n {/* Overlay info */}\n <div className=\"absolute top-3 left-3 flex items-center gap-2\">\n <Badge className=\"text-[10px]\" style={{ background: \"color-mix(in srgb, var(--gray-950) 70%, transparent)\", color: \"var(--gray-50)\", backdropFilter: \"blur(4px)\" }}>\n {trainee.name} · Operating\n </Badge>\n </div>\n <div className=\"absolute top-3 right-3 flex items-center gap-2\">\n <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Maximize\" style={{ background: \"color-mix(in srgb, var(--gray-950) 50%, transparent)\", color: \"var(--gray-50)\" }}>\n <Maximize2 size={13} />\n </Button>\n </div>\n\n {/* Connectivity loss overlay */}\n {connectionLost && (\n <div className=\"absolute inset-0 flex items-center justify-center\" style={{ background: \"color-mix(in srgb, var(--red-500) 80%, transparent)\", backdropFilter: \"blur(2px)\" }}>\n <div className=\"text-center max-w-md p-6\">\n <AlertTriangle size={48} strokeWidth={1.5} className=\"mx-auto mb-3\" />\n <h2 className=\"text-xl font-bold mb-2\">FAIL-SAFE ACTIVATED</h2>\n <p className=\"text-[14px] mb-4\">Connection lost. Trainee should revert to local supervision immediately. Reconnection in progress…</p>\n <Button variant=\"default\" onClick={() => setConnectionLost(false)} style={{ background: \"var(--gray-0)\", color: \"var(--red-500)\" }}>\n Mark resolved\n </Button>\n </div>\n </div>\n )}\n </div>\n\n {/* Participant strip */}\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-3\">\n {participants.filter((p) => !p.hasFeed).map((p) => (\n <ParticipantTile key={p.id} p={p} onToggleMic={() => toggleMic(p.id)} isMe={p.id === me.id} />\n ))}\n </div>\n </div>\n\n {/* Sidebar */}\n <aside className=\"w-80 flex flex-col gap-3 shrink-0\">\n {/* Participants */}\n <Card className=\"flex-1 overflow-hidden\" style={{ background: \"color-mix(in srgb, var(--gray-0) 4%, transparent)\", border: \"1px solid color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <div className=\"px-4 py-3 flex items-center justify-between border-b\" style={{ borderColor: \"color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <h3 className=\"text-[12px] font-semibold uppercase tracking-wider flex items-center gap-2\">\n <Users size={12} /> Participants ({participants.length})\n </h3>\n </div>\n <div className=\"p-2 space-y-1\">\n {participants.map((p) => (\n <div key={p.id} className=\"px-2 py-2 rounded-md flex items-center gap-2.5\">\n <div className=\"w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: ROLE_META[p.role].bg, color: ROLE_META[p.role].color }}>\n {p.initials}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[12px] truncate\" style={{ color: \"var(--gray-50)\" }}>{p.name}</div>\n <div className=\"text-[10px] truncate\" style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }}>{ROLE_META[p.role].label}</div>\n </div>\n <div className=\"flex items-center gap-1\">\n {p.micOn ? <Mic size={11} style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }} /> : <MicOff size={11} style={{ color: \"color-mix(in srgb, var(--gray-0) 30%, transparent)\" }} />}\n {p.videoOn ? <VideoIcon size={11} style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }} /> : <VideoOff size={11} style={{ color: \"color-mix(in srgb, var(--gray-0) 30%, transparent)\" }} />}\n </div>\n </div>\n ))}\n </div>\n </Card>\n\n {/* Safety events */}\n <Card className=\"overflow-hidden\" style={{ background: \"color-mix(in srgb, var(--gray-0) 4%, transparent)\", border: \"1px solid color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <div className=\"px-4 py-3 flex items-center justify-between border-b\" style={{ borderColor: \"color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <h3 className=\"text-[12px] font-semibold uppercase tracking-wider\">Safety events</h3>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setSafetyOpen(true)} style={{ color: \"var(--amber-500)\" }}>\n <Flag size={11} /> Flag\n </Button>\n </div>\n <div className=\"p-3\">\n {safetyEvents.length === 0 ? (\n <p className=\"text-[11px]\" style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }}>No events flagged.</p>\n ) : (\n <ul className=\"space-y-2 text-[11px]\">\n {safetyEvents.map((e, i) => (\n <li key={i} className=\"flex items-start gap-2\">\n <Flag size={10} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--amber-500)\" }} />\n <div>\n <div className=\"font-mono tabular-nums\" style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }}>{Math.floor(e.at / 60)}:{String(e.at % 60).padStart(2, \"0\")}</div>\n <div style={{ color: \"var(--gray-50)\" }}>{e.note}</div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n </Card>\n\n {/* Connectivity test toggle (demo) */}\n <Button variant=\"outline\" size=\"sm\" onClick={() => setConnectionLost(true)} style={{ borderColor: \"color-mix(in srgb, var(--gray-0) 15%, transparent)\", color: \"var(--gray-50)\", background: \"transparent\" }}>\n Simulate connection loss\n </Button>\n </aside>\n </main>\n\n {/* Bottom controls */}\n <footer className=\"h-16 px-6 flex items-center justify-center gap-2 shrink-0 border-t\" style={{ background: \"color-mix(in srgb, var(--gray-950) 80%, transparent)\", borderColor: \"color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <Button\n variant=\"ghost\"\n size=\"icon-lg\"\n onClick={() => toggleMic(me.id)}\n aria-label=\"Mute\"\n style={{ background: me.micOn ? \"color-mix(in srgb, var(--gray-0) 8%, transparent)\" : \"var(--red-500)\", color: \"var(--gray-50)\" }}\n >\n {me.micOn ? <Mic size={16} /> : <MicOff size={16} />}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"icon-lg\"\n aria-label=\"Camera\"\n style={{ background: me.videoOn ? \"color-mix(in srgb, var(--gray-0) 8%, transparent)\" : \"var(--red-500)\", color: \"var(--gray-50)\" }}\n >\n {me.videoOn ? <VideoIcon size={16} /> : <VideoOff size={16} />}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"icon-lg\"\n onClick={() => setSafetyOpen(true)}\n aria-label=\"Flag safety event\"\n style={{ background: \"color-mix(in srgb, var(--gray-0) 8%, transparent)\", color: \"var(--amber-500)\" }}\n >\n <Flag size={16} />\n </Button>\n </footer>\n\n {/* Safety event modal */}\n {safetyOpen && (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\" style={{ background: \"color-mix(in srgb, var(--gray-950) 80%, transparent)\", backdropFilter: \"blur(4px)\" }}>\n <div className=\"w-full max-w-md rounded-2xl p-6\" style={{ background: \"var(--gray-900)\", border: \"1px solid color-mix(in srgb, var(--gray-0) 10%, transparent)\" }}>\n <div className=\"flex items-start gap-3 mb-4\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center shrink-0\" style={{ background: \"color-mix(in srgb, var(--amber-500) 20%, transparent)\", color: \"var(--amber-500)\" }}>\n <Flag size={18} strokeWidth={1.8} />\n </div>\n <div>\n <h3 className=\"text-base font-semibold mb-1\" style={{ color: \"var(--gray-50)\" }}>Flag safety event</h3>\n <p className=\"text-[12px]\" style={{ color: \"color-mix(in srgb, var(--gray-0) 60%, transparent)\" }}>\n Clinical responsibility remains with the operating surgeon. Provide guidance as appropriate.\n </p>\n </div>\n </div>\n <Textarea\n value={safetyNote}\n onChange={(e) => setSafetyNote(e.target.value.slice(0, 200))}\n placeholder=\"Brief description (max 200 chars)…\"\n rows={3}\n />\n <p className=\"text-[10px] mt-1 mb-4\" style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }}>\n {safetyNote.length} / 200\n </p>\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"outline\" onClick={() => { setSafetyOpen(false); setSafetyNote(\"\"); }}>Cancel</Button>\n <Button variant=\"brand\" disabled={!safetyNote.trim()} onClick={() => {\n setSafetyEvents([...safetyEvents, { at: elapsed, note: safetyNote }]);\n setSafetyOpen(false); setSafetyNote(\"\");\n }}>\n <Flag size={11} /> Log event\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n );\n}\n\nfunction ParticipantTile({ p, onToggleMic, isMe }: { p: Participant; onToggleMic: () => void; isMe: boolean }) {\n const meta = ROLE_META[p.role];\n return (\n <div className=\"rounded-xl relative overflow-hidden flex items-center justify-center\" style={{ background: \"color-mix(in srgb, var(--gray-0) 5%, transparent)\", border: \"1px solid color-mix(in srgb, var(--gray-0) 8%, transparent)\" }}>\n <div className=\"w-12 h-12 rounded-full flex items-center justify-center text-[14px] font-semibold\" style={{ background: meta.bg, color: meta.color }}>\n {p.initials}\n </div>\n <div className=\"absolute bottom-2 left-2 right-2 flex items-center justify-between\">\n <div className=\"min-w-0 flex-1\">\n <div className=\"text-[11px] font-medium truncate\" style={{ color: \"var(--gray-50)\" }}>\n {p.name.split(\" \").slice(0, 2).join(\" \")}{isMe && \" (You)\"}\n </div>\n <div className=\"text-[9px] truncate\" style={{ color: meta.color }}>{meta.label}</div>\n </div>\n <div className=\"flex items-center gap-1 shrink-0\">\n {p.micOn\n ? <Mic size={10} style={{ color: \"var(--gray-50)\" }} />\n : <MicOff size={10} style={{ color: \"color-mix(in srgb, var(--gray-0) 40%, transparent)\" }} />\n }\n </div>\n </div>\n </div>\n );\n}\n"
48
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/sponsor/billing/page.tsx",
4
+ "sha256": "cd72c7698880bb1f2b07fd5f79151e71dc3887ee96bdd020c3cc3b4f9c7893df",
5
+ "imports": [
6
+ "lucide-react",
7
+ "@/components/ui/button",
8
+ "@/components/ui/card",
9
+ "@/components/ui/badge",
10
+ "@/components/ui/table",
11
+ "@/components/portal/portal-shell",
12
+ "@/components/portal/empty-state",
13
+ "@/lib/credits"
14
+ ],
15
+ "tokens": [
16
+ "--color-bg-subtle",
17
+ "--color-text-muted",
18
+ "--color-text-secondary"
19
+ ],
20
+ "fragments": [
21
+ {
22
+ "id": "SponsorBillingPage",
23
+ "line": 16,
24
+ "code": "export default function SponsorBillingPage() {\n return (\n <PortalShell kind=\"sponsor\">\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Billing\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Invoices, receipts, and payment history.\n </p>\n </div>\n </div>\n\n {INVOICES.length === 0 ? (\n <EmptyState icon={Receipt} title=\"No invoices yet\" description=\"Your first invoice will appear here after subscription activation.\" />\n ) : (\n <Card className=\"p-0\">\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Invoice</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Description</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Amount</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Status</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Action</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {INVOICES.map((inv) => (\n <TableRow key={inv.id}>\n <TableCell className=\"px-5 py-3 text-[12px] font-mono\">{inv.id}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\">{inv.at}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\">{inv.description}</TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums font-medium\">{gbp(inv.amount)}</TableCell>\n <TableCell className=\"px-5 py-3\"><Badge className=\"text-[10px]\">{inv.status}</Badge></TableCell>\n <TableCell className=\"px-5 py-3 text-right\">\n <Button variant=\"outline\" size=\"sm\"><Download size={11} /> PDF</Button>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </Card>\n )}\n </PortalShell>\n );\n}"
25
+ }
26
+ ],
27
+ "content": "\"use client\";\n\nimport { Receipt, Download } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from \"@/components/ui/table\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { EmptyState } from \"@/components/portal/empty-state\";\nimport { gbp } from \"@/lib/credits\";\n\nconst INVOICES = [\n { id: \"INV-2026-0124\", at: \"2026-01-15\", description: \"Growth package — annual + platform access\", amount: 39_000, status: \"Paid\" },\n];\n\nexport default function SponsorBillingPage() {\n return (\n <PortalShell kind=\"sponsor\">\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Billing\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Invoices, receipts, and payment history.\n </p>\n </div>\n </div>\n\n {INVOICES.length === 0 ? (\n <EmptyState icon={Receipt} title=\"No invoices yet\" description=\"Your first invoice will appear here after subscription activation.\" />\n ) : (\n <Card className=\"p-0\">\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Invoice</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Description</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Amount</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Status</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Action</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {INVOICES.map((inv) => (\n <TableRow key={inv.id}>\n <TableCell className=\"px-5 py-3 text-[12px] font-mono\">{inv.id}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\">{inv.at}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\">{inv.description}</TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums font-medium\">{gbp(inv.amount)}</TableCell>\n <TableCell className=\"px-5 py-3\"><Badge className=\"text-[10px]\">{inv.status}</Badge></TableCell>\n <TableCell className=\"px-5 py-3 text-right\">\n <Button variant=\"outline\" size=\"sm\"><Download size={11} /> PDF</Button>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </Card>\n )}\n </PortalShell>\n );\n}\n"
28
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/sponsor/profile/page.tsx",
4
+ "sha256": "c1c66dc8adbe7832ce5d85a09572215ccd9f354a171ddeccf476f6c19509e06e",
5
+ "imports": [
6
+ "react",
7
+ "@/components/portal/portal-shell",
8
+ "@/components/portal/profile-form",
9
+ "@/components/auth/auth-primitives",
10
+ "@/components/auth/onboarding-primitives",
11
+ "@/components/ui/input"
12
+ ],
13
+ "tokens": [],
14
+ "fragments": [
15
+ {
16
+ "id": "SponsorProfilePage",
17
+ "line": 24,
18
+ "code": "export default function SponsorProfilePage() {\n const [company, setCompany] = useState(\"Olympus Medical UK\");\n const [industry, setIndustry] = useState(\"medtech\");\n const [region, setRegion] = useState(\"GB\");\n const [website, setWebsite] = useState(\"https://olympus.co.uk\");\n const [billingEmail, setBillingEmail] = useState(\"billing@olympus.co.uk\");\n\n return (\n <PortalShell kind=\"sponsor\">\n <ProfileForm\n title=\"Profile\"\n subtitle=\"Update your photo, contact details, company info, and billing email.\"\n initials=\"HC\"\n matchingTitle=\"Organisation profile\"\n matchingDescription=\"This information appears on invoices, audit logs, and analytics shared with your trainees.\"\n initialFirstName=\"Helen\"\n initialLastName=\"Chen\"\n initialEmail=\"h.chen@olympus.co.uk\"\n fields={\n <>\n <Field label=\"Company name\" htmlFor=\"co\">\n <Input id=\"co\" value={company} onChange={(e) => setCompany(e.target.value)} className=\"h-10\" />\n </Field>\n <Field label=\"Industry\" htmlFor=\"ind\">\n <Select id=\"ind\" value={industry} onChange={setIndustry} options={INDUSTRIES} />\n </Field>\n <Field label=\"Region\" htmlFor=\"region\">\n <Select id=\"region\" value={region} onChange={setRegion} options={REGIONS} />\n </Field>\n <Field label=\"Company website\" htmlFor=\"web\" optional>\n <Input id=\"web\" type=\"url\" value={website} onChange={(e) => setWebsite(e.target.value)} className=\"h-10\" />\n </Field>\n <Field label=\"Billing email\" htmlFor=\"be\" hint=\"Invoices and payment receipts go here.\">\n <Input id=\"be\" type=\"email\" value={billingEmail} onChange={(e) => setBillingEmail(e.target.value)} className=\"h-10\" />\n </Field>\n </>\n }\n />\n </PortalShell>\n );\n}"
19
+ }
20
+ ],
21
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { ProfileForm } from \"@/components/portal/profile-form\";\nimport { Field } from \"@/components/auth/auth-primitives\";\nimport { Select } from \"@/components/auth/onboarding-primitives\";\nimport { Input } from \"@/components/ui/input\";\n\nconst REGIONS = [\n { value: \"GB\", label: \"United Kingdom\" },\n { value: \"EU\", label: \"European Union\" },\n { value: \"US\", label: \"United States\" },\n { value: \"APAC\", label: \"Asia-Pacific\" },\n];\n\nconst INDUSTRIES = [\n { value: \"medtech\", label: \"Medical Device / MedTech\" },\n { value: \"pharma\", label: \"Pharmaceutical\" },\n { value: \"hospital\", label: \"Hospital / Trust\" },\n { value: \"society\", label: \"Society / Association\" },\n];\n\nexport default function SponsorProfilePage() {\n const [company, setCompany] = useState(\"Olympus Medical UK\");\n const [industry, setIndustry] = useState(\"medtech\");\n const [region, setRegion] = useState(\"GB\");\n const [website, setWebsite] = useState(\"https://olympus.co.uk\");\n const [billingEmail, setBillingEmail] = useState(\"billing@olympus.co.uk\");\n\n return (\n <PortalShell kind=\"sponsor\">\n <ProfileForm\n title=\"Profile\"\n subtitle=\"Update your photo, contact details, company info, and billing email.\"\n initials=\"HC\"\n matchingTitle=\"Organisation profile\"\n matchingDescription=\"This information appears on invoices, audit logs, and analytics shared with your trainees.\"\n initialFirstName=\"Helen\"\n initialLastName=\"Chen\"\n initialEmail=\"h.chen@olympus.co.uk\"\n fields={\n <>\n <Field label=\"Company name\" htmlFor=\"co\">\n <Input id=\"co\" value={company} onChange={(e) => setCompany(e.target.value)} className=\"h-10\" />\n </Field>\n <Field label=\"Industry\" htmlFor=\"ind\">\n <Select id=\"ind\" value={industry} onChange={setIndustry} options={INDUSTRIES} />\n </Field>\n <Field label=\"Region\" htmlFor=\"region\">\n <Select id=\"region\" value={region} onChange={setRegion} options={REGIONS} />\n </Field>\n <Field label=\"Company website\" htmlFor=\"web\" optional>\n <Input id=\"web\" type=\"url\" value={website} onChange={(e) => setWebsite(e.target.value)} className=\"h-10\" />\n </Field>\n <Field label=\"Billing email\" htmlFor=\"be\" hint=\"Invoices and payment receipts go here.\">\n <Input id=\"be\" type=\"email\" value={billingEmail} onChange={(e) => setBillingEmail(e.target.value)} className=\"h-10\" />\n </Field>\n </>\n }\n />\n </PortalShell>\n );\n}\n"
22
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/sponsor/sessions/page.tsx",
4
+ "sha256": "759bffc829fb7d7ef9b5d6c500b3ca130be2f04420937f5b99461d460da25b07",
5
+ "imports": [
6
+ "next/link",
7
+ "lucide-react",
8
+ "@/components/ui/button",
9
+ "@/components/ui/badge",
10
+ "@/components/ui/card",
11
+ "@/components/ui/table",
12
+ "@/components/portal/portal-shell",
13
+ "@/components/portal/empty-state"
14
+ ],
15
+ "tokens": [
16
+ "--color-bg-subtle",
17
+ "--color-text-muted",
18
+ "--color-text-secondary"
19
+ ],
20
+ "fragments": [
21
+ {
22
+ "id": "SponsorSessionsPage",
23
+ "line": 18,
24
+ "code": "export default function SponsorSessionsPage() {\n return (\n <PortalShell kind=\"sponsor\">\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Sessions\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n All mentorship sessions delivered to your sponsored trainees.\n </p>\n </div>\n <Button variant=\"outline\"><Download size={13} /> Export CSV</Button>\n </div>\n\n {SESSIONS.length === 0 ? (\n <EmptyState icon={Calendar} title=\"No sessions yet\" description=\"When your sponsored trainees complete their first mentorship session, it will appear here.\" />\n ) : (\n <Card className=\"p-0\">\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Trainee</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Procedure</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Mentor</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Duration</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Credits</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Status</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {SESSIONS.map((s) => (\n <TableRow key={s.id}>\n <TableCell className=\"px-5 py-3 text-[12px]\">{s.date}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px] font-medium\">{s.trainee}</TableCell>\n <TableCell className=\"px-5 py-3\"><Badge variant=\"outline\" className=\"text-[10px]\">{s.procedure}</Badge></TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\">{s.mentor}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{s.duration}</TableCell>\n <TableCell className=\"px-5 py-3 text-right text-[12px] tabular-nums\">{s.credits}</TableCell>\n <TableCell className=\"px-5 py-3\"><Badge className=\"text-[10px]\">{s.status}</Badge></TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </Card>\n )}\n </PortalShell>\n );\n}"
25
+ }
26
+ ],
27
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { Calendar, Download } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card } from \"@/components/ui/card\";\nimport { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from \"@/components/ui/table\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { EmptyState } from \"@/components/portal/empty-state\";\n\nconst SESSIONS = [\n { id: \"ses-2401\", date: \"2026-05-22\", trainee: \"Dr. Hannah Liu\", procedure: \"ESD\", mentor: \"Prof. Amyn Haji\", duration: \"2h 45m\", credits: 1, status: \"Closed\" },\n { id: \"ses-2389\", date: \"2026-05-15\", trainee: \"Dr. Hannah Liu\", procedure: \"EMR\", mentor: \"Dr. Marie Dubois\", duration: \"1h 30m\", credits: 1, status: \"Closed\" },\n { id: \"ses-2360\", date: \"2026-04-22\", trainee: \"Dr. Lila Patel\", procedure: \"ERCP\", mentor: \"Prof. Amyn Haji\", duration: \"3h 15m\", credits: 2, status: \"Closed\" },\n];\n\nexport default function SponsorSessionsPage() {\n return (\n <PortalShell kind=\"sponsor\">\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Sessions\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n All mentorship sessions delivered to your sponsored trainees.\n </p>\n </div>\n <Button variant=\"outline\"><Download size={13} /> Export CSV</Button>\n </div>\n\n {SESSIONS.length === 0 ? (\n <EmptyState icon={Calendar} title=\"No sessions yet\" description=\"When your sponsored trainees complete their first mentorship session, it will appear here.\" />\n ) : (\n <Card className=\"p-0\">\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Trainee</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Procedure</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Mentor</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Duration</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Credits</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Status</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {SESSIONS.map((s) => (\n <TableRow key={s.id}>\n <TableCell className=\"px-5 py-3 text-[12px]\">{s.date}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px] font-medium\">{s.trainee}</TableCell>\n <TableCell className=\"px-5 py-3\"><Badge variant=\"outline\" className=\"text-[10px]\">{s.procedure}</Badge></TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\">{s.mentor}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{s.duration}</TableCell>\n <TableCell className=\"px-5 py-3 text-right text-[12px] tabular-nums\">{s.credits}</TableCell>\n <TableCell className=\"px-5 py-3\"><Badge className=\"text-[10px]\">{s.status}</Badge></TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </Card>\n )}\n </PortalShell>\n );\n}\n"
28
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/sponsor/settings/page.tsx",
4
+ "sha256": "080805bbbec74e34663141463f9f321276b67b1293db76997240dc27463e27d4",
5
+ "imports": [
6
+ "@/components/portal/portal-shell",
7
+ "@/components/portal/settings-view"
8
+ ],
9
+ "tokens": [],
10
+ "fragments": [
11
+ {
12
+ "id": "SponsorSettingsPage",
13
+ "line": 4,
14
+ "code": "export default function SponsorSettingsPage() {\n return (\n <PortalShell kind=\"sponsor\">\n <SettingsView\n kind=\"sponsor\"\n name=\"Helen Chen\"\n email=\"h.chen@olympus.co.uk\"\n id=\"MX-44D8E2\"\n />\n </PortalShell>\n );\n}"
15
+ }
16
+ ],
17
+ "content": "import { PortalShell } from \"@/components/portal/portal-shell\";\nimport { SettingsView } from \"@/components/portal/settings-view\";\n\nexport default function SponsorSettingsPage() {\n return (\n <PortalShell kind=\"sponsor\">\n <SettingsView\n kind=\"sponsor\"\n name=\"Helen Chen\"\n email=\"h.chen@olympus.co.uk\"\n id=\"MX-44D8E2\"\n />\n </PortalShell>\n );\n}\n"
18
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/sponsor/trainees/page.tsx",
4
+ "sha256": "b217a3396233d3961f36fc14aa39506568bcc4bf521cbeacdc954a0f0f725eb2",
5
+ "imports": [
6
+ "react",
7
+ "lucide-react",
8
+ "@/components/ui/input",
9
+ "@/components/ui/button",
10
+ "@/components/ui/badge",
11
+ "@/components/ui/card",
12
+ "@/components/ui/table",
13
+ "@/components/portal/portal-shell",
14
+ "@/lib/credits-mock"
15
+ ],
16
+ "tokens": [
17
+ "--color-bg-brand-muted",
18
+ "--color-bg-subtle",
19
+ "--color-border-default",
20
+ "--color-status-error-text",
21
+ "--color-text-brand",
22
+ "--color-text-muted",
23
+ "--color-text-primary",
24
+ "--color-text-secondary",
25
+ "--red-500",
26
+ "--teal-500"
27
+ ],
28
+ "fragments": [
29
+ {
30
+ "id": "SponsorTraineesPage",
31
+ "line": 13,
32
+ "code": "export default function SponsorTraineesPage() {\n const sub = SUBSCRIPTIONS[0];\n const trainees = ALLOCATIONS_BY_SUB[sub.id] ?? [];\n const [q, setQ] = useState(\"\");\n\n const filtered = trainees.filter((t) =>\n !q ||\n t.traineeName.toLowerCase().includes(q.toLowerCase()) ||\n t.institution.toLowerCase().includes(q.toLowerCase())\n );\n\n const totals = trainees.reduce((acc, t) => ({\n allocated: acc.allocated + t.allocated,\n consumed: acc.consumed + t.consumed,\n escrowed: acc.escrowed + t.escrowed,\n remaining: acc.remaining + t.remaining,\n }), { allocated: 0, consumed: 0, escrowed: 0, remaining: 0 });\n\n return (\n <PortalShell\n kind=\"sponsor\"\n account={{ name: sub.sponsorName, subtitle: sub.primaryContactName, initials: sub.sponsorName.split(\" \").slice(0, 2).map((s) => s[0]).join(\"\") }}\n >\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Sponsored trainees\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n {trainees.length} trainees · {totals.allocated} credits allocated · {totals.consumed} consumed\n </p>\n </div>\n <Button variant=\"outline\" size=\"default\">\n <Download size={13} /> Export CSV\n </Button>\n </div>\n\n {/* Search */}\n <div className=\"relative mb-4 max-w-md\">\n <Search size={14} className=\"absolute left-3 top-1/2 -translate-y-1/2\" style={{ color: \"var(--color-text-muted)\" }} />\n <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder=\"Search trainees, institutions…\" className=\"h-9 pl-9\" />\n {q && (\n <button onClick={() => setQ(\"\")} className=\"absolute right-3 top-1/2 -translate-y-1/2 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-muted)\" }}>\n <X size={13} />\n </button>\n )}\n </div>\n\n <Card className=\"p-0\">\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Trainee</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Procedures</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Allocated</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>In use</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Consumed</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Remaining</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Usage</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 && (\n <TableRow>\n <TableCell colSpan={7} className=\"text-center py-12\" style={{ color: \"var(--color-text-muted)\" }}>\n No trainees match \"{q}\".\n </TableCell>\n </TableRow>\n )}\n {filtered.map((t) => {\n const pct = t.allocated > 0 ? Math.round(((t.consumed + t.escrowed) / t.allocated) * 100) : 0;\n return (\n <TableRow key={t.traineeId}>\n <TableCell className=\"px-5 py-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n {t.traineeName.split(\" \").slice(-2).map((s) => s[0]).join(\"\").slice(0, 2)}\n </div>\n <div>\n <div className=\"font-medium text-[13px]\">{t.traineeName}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>{t.institution}</div>\n </div>\n </div>\n </TableCell>\n <TableCell className=\"px-5 py-3\">\n <div className=\"flex flex-wrap gap-1\">\n {t.procedures.map((p) => <Badge key={p} variant=\"outline\" className=\"text-[10px]\">{p}</Badge>)}\n </div>\n </TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums\">{t.allocated}</TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums\" style={{ color: \"var(--color-text-secondary)\" }}>{t.escrowed}</TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums\" style={{ color: \"var(--color-text-secondary)\" }}>{t.consumed}</TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums font-medium\" style={{ color: t.remaining === 0 ? \"var(--color-status-error-text)\" : \"var(--color-text-primary)\" }}>\n {t.remaining}\n </TableCell>\n <TableCell className=\"px-5 py-3 w-32\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex-1 h-1.5 rounded-full overflow-hidden\" style={{ background: \"var(--color-border-default)\" }}>\n <div className=\"h-full\" style={{ width: `${pct}%`, background: pct >= 100 ? \"var(--red-500)\" : \"var(--teal-500)\" }} />\n </div>\n <span className=\"text-[11px] tabular-nums w-8 text-right\" style={{ color: \"var(--color-text-muted)\" }}>{pct}%</span>\n </div>\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </Card>\n </PortalShell>\n );\n}"
33
+ }
34
+ ],
35
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Search, Download, X, ArrowRight } from \"lucide-react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardHeader, CardTitle, CardDescription } from \"@/components/ui/card\";\nimport { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from \"@/components/ui/table\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { ALLOCATIONS_BY_SUB, SUBSCRIPTIONS } from \"@/lib/credits-mock\";\n\nexport default function SponsorTraineesPage() {\n const sub = SUBSCRIPTIONS[0];\n const trainees = ALLOCATIONS_BY_SUB[sub.id] ?? [];\n const [q, setQ] = useState(\"\");\n\n const filtered = trainees.filter((t) =>\n !q ||\n t.traineeName.toLowerCase().includes(q.toLowerCase()) ||\n t.institution.toLowerCase().includes(q.toLowerCase())\n );\n\n const totals = trainees.reduce((acc, t) => ({\n allocated: acc.allocated + t.allocated,\n consumed: acc.consumed + t.consumed,\n escrowed: acc.escrowed + t.escrowed,\n remaining: acc.remaining + t.remaining,\n }), { allocated: 0, consumed: 0, escrowed: 0, remaining: 0 });\n\n return (\n <PortalShell\n kind=\"sponsor\"\n account={{ name: sub.sponsorName, subtitle: sub.primaryContactName, initials: sub.sponsorName.split(\" \").slice(0, 2).map((s) => s[0]).join(\"\") }}\n >\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Sponsored trainees\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n {trainees.length} trainees · {totals.allocated} credits allocated · {totals.consumed} consumed\n </p>\n </div>\n <Button variant=\"outline\" size=\"default\">\n <Download size={13} /> Export CSV\n </Button>\n </div>\n\n {/* Search */}\n <div className=\"relative mb-4 max-w-md\">\n <Search size={14} className=\"absolute left-3 top-1/2 -translate-y-1/2\" style={{ color: \"var(--color-text-muted)\" }} />\n <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder=\"Search trainees, institutions…\" className=\"h-9 pl-9\" />\n {q && (\n <button onClick={() => setQ(\"\")} className=\"absolute right-3 top-1/2 -translate-y-1/2 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-muted)\" }}>\n <X size={13} />\n </button>\n )}\n </div>\n\n <Card className=\"p-0\">\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Trainee</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Procedures</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Allocated</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>In use</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Consumed</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Remaining</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Usage</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 && (\n <TableRow>\n <TableCell colSpan={7} className=\"text-center py-12\" style={{ color: \"var(--color-text-muted)\" }}>\n No trainees match \"{q}\".\n </TableCell>\n </TableRow>\n )}\n {filtered.map((t) => {\n const pct = t.allocated > 0 ? Math.round(((t.consumed + t.escrowed) / t.allocated) * 100) : 0;\n return (\n <TableRow key={t.traineeId}>\n <TableCell className=\"px-5 py-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n {t.traineeName.split(\" \").slice(-2).map((s) => s[0]).join(\"\").slice(0, 2)}\n </div>\n <div>\n <div className=\"font-medium text-[13px]\">{t.traineeName}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>{t.institution}</div>\n </div>\n </div>\n </TableCell>\n <TableCell className=\"px-5 py-3\">\n <div className=\"flex flex-wrap gap-1\">\n {t.procedures.map((p) => <Badge key={p} variant=\"outline\" className=\"text-[10px]\">{p}</Badge>)}\n </div>\n </TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums\">{t.allocated}</TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums\" style={{ color: \"var(--color-text-secondary)\" }}>{t.escrowed}</TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums\" style={{ color: \"var(--color-text-secondary)\" }}>{t.consumed}</TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums font-medium\" style={{ color: t.remaining === 0 ? \"var(--color-status-error-text)\" : \"var(--color-text-primary)\" }}>\n {t.remaining}\n </TableCell>\n <TableCell className=\"px-5 py-3 w-32\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex-1 h-1.5 rounded-full overflow-hidden\" style={{ background: \"var(--color-border-default)\" }}>\n <div className=\"h-full\" style={{ width: `${pct}%`, background: pct >= 100 ? \"var(--red-500)\" : \"var(--teal-500)\" }} />\n </div>\n <span className=\"text-[11px] tabular-nums w-8 text-right\" style={{ color: \"var(--color-text-muted)\" }}>{pct}%</span>\n </div>\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </Card>\n </PortalShell>\n );\n}\n"
36
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/sponsor/page.tsx",
4
+ "sha256": "4d31f28f1533c9b8645c6c7e1eb79ad622cd730b8062b7d6e62669b0a4c9ea76",
5
+ "imports": [
6
+ "next/link",
7
+ "lucide-react",
8
+ "@/components/ui/button",
9
+ "@/components/ui/badge",
10
+ "@/components/ui/card",
11
+ "@/components/ui/separator",
12
+ "@/components/portal/portal-shell",
13
+ "@/components/portal/usage-sparkline",
14
+ "@/components/auth/auth-primitives",
15
+ "@/lib/credits-mock",
16
+ "@/lib/credits"
17
+ ],
18
+ "tokens": [
19
+ "--color-bg-brand-muted",
20
+ "--color-bg-subtle",
21
+ "--color-border-default",
22
+ "--color-text-brand",
23
+ "--color-text-muted",
24
+ "--color-text-primary",
25
+ "--color-text-secondary",
26
+ "--teal-300",
27
+ "--teal-500"
28
+ ],
29
+ "fragments": [
30
+ {
31
+ "id": "SponsorOverviewPage",
32
+ "line": 17,
33
+ "code": "export default function SponsorOverviewPage() {\n // Demo: use Olympus subscription\n const sub = SUBSCRIPTIONS[0];\n const tier = TIERS[sub.tier];\n const remaining = creditsRemaining(sub);\n const util = utilization(sub);\n const daysLeft = daysFrom(sub.endDate);\n const lowCredits = remaining / sub.totalCredits <= 0.1;\n\n return (\n <PortalShell\n kind=\"sponsor\"\n account={{ name: sub.sponsorName, subtitle: sub.primaryContactName, initials: sub.sponsorName.split(\" \").slice(0, 2).map((s) => s[0]).join(\"\") }}\n >\n {/* Heading + key actions */}\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Subscription overview\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n {tier.name} package · expires in {daysLeft} days\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" size=\"default\">\n <Download size={13} /> Export report\n </Button>\n <Button variant=\"brand\" size=\"default\" render={<Link href=\"#\" />}>\n Top up credits <ArrowRight size={13} />\n </Button>\n </div>\n </div>\n\n {/* Low credit warning */}\n {lowCredits && (\n <div className=\"mb-5\">\n <InlineAlert\n variant=\"warning\"\n title=\"Running low on credits\"\n action={<Button variant=\"outline\" size=\"sm\">Contact sales</Button>}\n >\n Only {remaining} of {sub.totalCredits} credits remaining. Trainees won't be able to request new sessions once depleted.\n </InlineAlert>\n </div>\n )}\n\n {/* Subscription summary card */}\n <Card className=\"mb-6\">\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <CardTitle className=\"text-base\">{tier.name} subscription</CardTitle>\n <Badge variant=\"outline\" className=\"capitalize\">{sub.status}</Badge>\n </div>\n <span className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>\n Sub ID: <code style={{ color: \"var(--color-text-secondary)\" }}>{sub.id}</code>\n </span>\n </div>\n <CardDescription>\n {tier.sessions === \"unlimited\" ? \"Unlimited sessions\" : `${tier.sessions} sessions`} · {gbp(tier.perSessionPrice)}/session · annual access fee {gbp(sub.platformAccessFee)}\n </CardDescription>\n </CardHeader>\n\n <CardContent>\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <MetricBlock label=\"Purchased\" value={sub.totalCredits.toString()} sub=\"credits\" />\n <MetricBlock label=\"Allocated\" value={sub.creditsAllocated.toString()} sub={`to ${ALLOCATIONS_BY_SUB[sub.id]?.length ?? 0} trainees`} />\n <MetricBlock label=\"In use\" value={sub.creditsEscrowed.toString()} sub=\"pending sessions\" />\n <MetricBlock label=\"Consumed\" value={sub.creditsConsumed.toString()} sub=\"sessions closed\" />\n </div>\n\n <Separator className=\"my-5\" />\n\n {/* Usage bar */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-[11px] font-medium uppercase tracking-wide\" style={{ color: \"var(--color-text-muted)\" }}>\n Utilisation\n </span>\n <span className=\"text-[12px] tabular-nums\" style={{ color: \"var(--color-text-secondary)\" }}>\n {sub.creditsConsumed} / {sub.totalCredits} consumed · {util}%\n </span>\n </div>\n <div className=\"h-2 rounded-full overflow-hidden flex\" style={{ background: \"var(--color-border-default)\" }}>\n <div className=\"h-full\" style={{ width: `${(sub.creditsConsumed / sub.totalCredits) * 100}%`, background: \"var(--teal-500)\" }} />\n <div className=\"h-full\" style={{ width: `${(sub.creditsEscrowed / sub.totalCredits) * 100}%`, background: \"var(--teal-300)\" }} />\n <div className=\"h-full\" style={{ width: `${((sub.creditsAllocated - sub.creditsConsumed - sub.creditsEscrowed) / sub.totalCredits) * 100}%`, background: \"color-mix(in srgb, var(--teal-500) 25%, transparent)\" }} />\n </div>\n <div className=\"flex items-center gap-4 mt-3 text-[11px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <LegendDot color=\"var(--teal-500)\" label=\"Consumed\" />\n <LegendDot color=\"var(--teal-300)\" label=\"In use\" />\n <LegendDot color=\"color-mix(in srgb, var(--teal-500) 25%, transparent)\" label=\"Allocated, unused\" />\n <LegendDot color=\"var(--color-border-default)\" label={`Remaining (${remaining})`} />\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Trend chart + tier details */}\n <div className=\"grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-6 mb-6\">\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base flex items-center gap-2\">\n <TrendingUp size={15} strokeWidth={1.7} /> Consumption trend\n </CardTitle>\n <Badge variant=\"ghost\" className=\"text-[10px]\">8 months</Badge>\n </div>\n <CardDescription>Cumulative credits consumed since subscription start.</CardDescription>\n </CardHeader>\n <CardContent>\n <UsageSparkline data={CONSUMPTION_HISTORY[sub.id] ?? []} max={sub.totalCredits} />\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base flex items-center gap-2\">\n <CalendarIcon size={15} strokeWidth={1.7} /> Subscription details\n </CardTitle>\n </CardHeader>\n <CardContent>\n <dl className=\"space-y-3 text-[12px]\">\n <Row label=\"Tier\" value={tier.name} />\n <Row label=\"Start\" value={sub.startDate} />\n <Row label=\"End\" value={`${sub.endDate} (${daysLeft}d left)`} />\n <Row label=\"Package fee\" value={gbp(sub.packageFee)} />\n <Row label=\"Access fee\" value={gbp(sub.platformAccessFee)} />\n <Row label=\"Total billed\" value={gbp(sub.packageFee + sub.platformAccessFee)} />\n </dl>\n </CardContent>\n </Card>\n </div>\n\n {/* Allocations preview */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"text-base\">Allocations</CardTitle>\n <CardDescription>{(ALLOCATIONS_BY_SUB[sub.id] ?? []).length} sponsored trainees</CardDescription>\n </div>\n <Button variant=\"outline\" size=\"sm\" render={<Link href=\"/portal/sponsor/trainees\" />}>\n View all <ArrowRight size={11} />\n </Button>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-2\">\n {(ALLOCATIONS_BY_SUB[sub.id] ?? []).slice(0, 4).map((a) => (\n <Link\n key={a.traineeId}\n href={`/portal/sponsor/trainees`}\n className=\"rounded-xl border p-3 flex items-center gap-3 transition-colors hover:bg-[var(--color-bg-subtle)]\"\n style={{ borderColor: \"var(--color-border-default)\" }}\n >\n <div className=\"w-9 h-9 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n {a.traineeName.split(\" \").slice(-2).map((s) => s[0]).join(\"\").slice(0, 2)}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium truncate\">{a.traineeName}</div>\n <div className=\"text-[11px] truncate\" style={{ color: \"var(--color-text-muted)\" }}>{a.institution}</div>\n </div>\n <div className=\"text-right shrink-0\">\n <div className=\"text-[12px] font-medium tabular-nums\">{a.remaining} / {a.allocated}</div>\n <div className=\"text-[10px]\" style={{ color: \"var(--color-text-muted)\" }}>credits left</div>\n </div>\n </Link>\n ))}\n </div>\n </CardContent>\n </Card>\n </PortalShell>\n );\n}"
34
+ },
35
+ {
36
+ "id": "MetricBlock",
37
+ "line": 196,
38
+ "code": "function MetricBlock({ label, value, sub }: { label: string; value: string; sub: string }) {\n return (\n <div>\n <div className=\"text-[10px] font-semibold uppercase tracking-wide mb-1\" style={{ color: \"var(--color-text-muted)\" }}>{label}</div>\n <div className=\"text-3xl font-semibold tabular-nums\" style={{ letterSpacing: \"-0.02em\" }}>{value}</div>\n <div className=\"text-[11px] mt-0.5\" style={{ color: \"var(--color-text-muted)\" }}>{sub}</div>\n </div>\n );\n}"
39
+ },
40
+ {
41
+ "id": "LegendDot",
42
+ "line": 206,
43
+ "code": "function LegendDot({ color, label }: { color: string; label: string }) {\n return (\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"w-2 h-2 rounded-full\" style={{ background: color }} />\n {label}\n </span>\n );\n}"
44
+ },
45
+ {
46
+ "id": "Row",
47
+ "line": 215,
48
+ "code": "function Row({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex items-center justify-between\">\n <dt style={{ color: \"var(--color-text-muted)\" }}>{label}</dt>\n <dd style={{ color: \"var(--color-text-primary)\" }}>{value}</dd>\n </div>\n );\n}"
49
+ }
50
+ ],
51
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { ArrowRight, Coins, Download, AlertCircle, Calendar as CalendarIcon, TrendingUp } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardHeader, CardTitle, CardDescription, CardContent } from \"@/components/ui/card\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { UsageSparkline } from \"@/components/portal/usage-sparkline\";\nimport { InlineAlert } from \"@/components/auth/auth-primitives\";\nimport {\n SUBSCRIPTIONS, ALLOCATIONS_BY_SUB, CONSUMPTION_HISTORY,\n} from \"@/lib/credits-mock\";\nimport { TIERS, creditsRemaining, utilization, gbp, daysFrom } from \"@/lib/credits\";\n\nexport default function SponsorOverviewPage() {\n // Demo: use Olympus subscription\n const sub = SUBSCRIPTIONS[0];\n const tier = TIERS[sub.tier];\n const remaining = creditsRemaining(sub);\n const util = utilization(sub);\n const daysLeft = daysFrom(sub.endDate);\n const lowCredits = remaining / sub.totalCredits <= 0.1;\n\n return (\n <PortalShell\n kind=\"sponsor\"\n account={{ name: sub.sponsorName, subtitle: sub.primaryContactName, initials: sub.sponsorName.split(\" \").slice(0, 2).map((s) => s[0]).join(\"\") }}\n >\n {/* Heading + key actions */}\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Subscription overview\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n {tier.name} package · expires in {daysLeft} days\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" size=\"default\">\n <Download size={13} /> Export report\n </Button>\n <Button variant=\"brand\" size=\"default\" render={<Link href=\"#\" />}>\n Top up credits <ArrowRight size={13} />\n </Button>\n </div>\n </div>\n\n {/* Low credit warning */}\n {lowCredits && (\n <div className=\"mb-5\">\n <InlineAlert\n variant=\"warning\"\n title=\"Running low on credits\"\n action={<Button variant=\"outline\" size=\"sm\">Contact sales</Button>}\n >\n Only {remaining} of {sub.totalCredits} credits remaining. Trainees won't be able to request new sessions once depleted.\n </InlineAlert>\n </div>\n )}\n\n {/* Subscription summary card */}\n <Card className=\"mb-6\">\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <CardTitle className=\"text-base\">{tier.name} subscription</CardTitle>\n <Badge variant=\"outline\" className=\"capitalize\">{sub.status}</Badge>\n </div>\n <span className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>\n Sub ID: <code style={{ color: \"var(--color-text-secondary)\" }}>{sub.id}</code>\n </span>\n </div>\n <CardDescription>\n {tier.sessions === \"unlimited\" ? \"Unlimited sessions\" : `${tier.sessions} sessions`} · {gbp(tier.perSessionPrice)}/session · annual access fee {gbp(sub.platformAccessFee)}\n </CardDescription>\n </CardHeader>\n\n <CardContent>\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <MetricBlock label=\"Purchased\" value={sub.totalCredits.toString()} sub=\"credits\" />\n <MetricBlock label=\"Allocated\" value={sub.creditsAllocated.toString()} sub={`to ${ALLOCATIONS_BY_SUB[sub.id]?.length ?? 0} trainees`} />\n <MetricBlock label=\"In use\" value={sub.creditsEscrowed.toString()} sub=\"pending sessions\" />\n <MetricBlock label=\"Consumed\" value={sub.creditsConsumed.toString()} sub=\"sessions closed\" />\n </div>\n\n <Separator className=\"my-5\" />\n\n {/* Usage bar */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-[11px] font-medium uppercase tracking-wide\" style={{ color: \"var(--color-text-muted)\" }}>\n Utilisation\n </span>\n <span className=\"text-[12px] tabular-nums\" style={{ color: \"var(--color-text-secondary)\" }}>\n {sub.creditsConsumed} / {sub.totalCredits} consumed · {util}%\n </span>\n </div>\n <div className=\"h-2 rounded-full overflow-hidden flex\" style={{ background: \"var(--color-border-default)\" }}>\n <div className=\"h-full\" style={{ width: `${(sub.creditsConsumed / sub.totalCredits) * 100}%`, background: \"var(--teal-500)\" }} />\n <div className=\"h-full\" style={{ width: `${(sub.creditsEscrowed / sub.totalCredits) * 100}%`, background: \"var(--teal-300)\" }} />\n <div className=\"h-full\" style={{ width: `${((sub.creditsAllocated - sub.creditsConsumed - sub.creditsEscrowed) / sub.totalCredits) * 100}%`, background: \"color-mix(in srgb, var(--teal-500) 25%, transparent)\" }} />\n </div>\n <div className=\"flex items-center gap-4 mt-3 text-[11px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <LegendDot color=\"var(--teal-500)\" label=\"Consumed\" />\n <LegendDot color=\"var(--teal-300)\" label=\"In use\" />\n <LegendDot color=\"color-mix(in srgb, var(--teal-500) 25%, transparent)\" label=\"Allocated, unused\" />\n <LegendDot color=\"var(--color-border-default)\" label={`Remaining (${remaining})`} />\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Trend chart + tier details */}\n <div className=\"grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-6 mb-6\">\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base flex items-center gap-2\">\n <TrendingUp size={15} strokeWidth={1.7} /> Consumption trend\n </CardTitle>\n <Badge variant=\"ghost\" className=\"text-[10px]\">8 months</Badge>\n </div>\n <CardDescription>Cumulative credits consumed since subscription start.</CardDescription>\n </CardHeader>\n <CardContent>\n <UsageSparkline data={CONSUMPTION_HISTORY[sub.id] ?? []} max={sub.totalCredits} />\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base flex items-center gap-2\">\n <CalendarIcon size={15} strokeWidth={1.7} /> Subscription details\n </CardTitle>\n </CardHeader>\n <CardContent>\n <dl className=\"space-y-3 text-[12px]\">\n <Row label=\"Tier\" value={tier.name} />\n <Row label=\"Start\" value={sub.startDate} />\n <Row label=\"End\" value={`${sub.endDate} (${daysLeft}d left)`} />\n <Row label=\"Package fee\" value={gbp(sub.packageFee)} />\n <Row label=\"Access fee\" value={gbp(sub.platformAccessFee)} />\n <Row label=\"Total billed\" value={gbp(sub.packageFee + sub.platformAccessFee)} />\n </dl>\n </CardContent>\n </Card>\n </div>\n\n {/* Allocations preview */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"text-base\">Allocations</CardTitle>\n <CardDescription>{(ALLOCATIONS_BY_SUB[sub.id] ?? []).length} sponsored trainees</CardDescription>\n </div>\n <Button variant=\"outline\" size=\"sm\" render={<Link href=\"/portal/sponsor/trainees\" />}>\n View all <ArrowRight size={11} />\n </Button>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-2\">\n {(ALLOCATIONS_BY_SUB[sub.id] ?? []).slice(0, 4).map((a) => (\n <Link\n key={a.traineeId}\n href={`/portal/sponsor/trainees`}\n className=\"rounded-xl border p-3 flex items-center gap-3 transition-colors hover:bg-[var(--color-bg-subtle)]\"\n style={{ borderColor: \"var(--color-border-default)\" }}\n >\n <div className=\"w-9 h-9 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n {a.traineeName.split(\" \").slice(-2).map((s) => s[0]).join(\"\").slice(0, 2)}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium truncate\">{a.traineeName}</div>\n <div className=\"text-[11px] truncate\" style={{ color: \"var(--color-text-muted)\" }}>{a.institution}</div>\n </div>\n <div className=\"text-right shrink-0\">\n <div className=\"text-[12px] font-medium tabular-nums\">{a.remaining} / {a.allocated}</div>\n <div className=\"text-[10px]\" style={{ color: \"var(--color-text-muted)\" }}>credits left</div>\n </div>\n </Link>\n ))}\n </div>\n </CardContent>\n </Card>\n </PortalShell>\n );\n}\n\n/* ─── Sub-components ─────────────────────────────────────────────────────── */\n\nfunction MetricBlock({ label, value, sub }: { label: string; value: string; sub: string }) {\n return (\n <div>\n <div className=\"text-[10px] font-semibold uppercase tracking-wide mb-1\" style={{ color: \"var(--color-text-muted)\" }}>{label}</div>\n <div className=\"text-3xl font-semibold tabular-nums\" style={{ letterSpacing: \"-0.02em\" }}>{value}</div>\n <div className=\"text-[11px] mt-0.5\" style={{ color: \"var(--color-text-muted)\" }}>{sub}</div>\n </div>\n );\n}\n\nfunction LegendDot({ color, label }: { color: string; label: string }) {\n return (\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"w-2 h-2 rounded-full\" style={{ background: color }} />\n {label}\n </span>\n );\n}\n\nfunction Row({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex items-center justify-between\">\n <dt style={{ color: \"var(--color-text-muted)\" }}>{label}</dt>\n <dd style={{ color: \"var(--color-text-primary)\" }}>{value}</dd>\n </div>\n );\n}\n"
52
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/trainee/competency/page.tsx",
4
+ "sha256": "3ef8296ff91edcf514c30e72b2816f28bea561691c229ae5101683c6552662b0",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "lucide-react",
9
+ "@/components/ui/button",
10
+ "@/components/ui/badge",
11
+ "@/components/ui/card",
12
+ "@/components/ui/tabs",
13
+ "@/components/ui/table",
14
+ "@/components/portal/portal-shell",
15
+ "@/components/portal/usage-sparkline",
16
+ "@/lib/sessions-mock",
17
+ "@/lib/sessions"
18
+ ],
19
+ "tokens": [
20
+ "--color-bg-brand-muted",
21
+ "--color-bg-subtle",
22
+ "--color-border-default",
23
+ "--color-text-brand",
24
+ "--color-text-muted",
25
+ "--color-text-primary",
26
+ "--color-text-secondary",
27
+ "--teal-500"
28
+ ],
29
+ "fragments": [
30
+ {
31
+ "id": "CompetencyPage",
32
+ "line": 32,
33
+ "code": "export default function CompetencyPage() {\n const log = COMPETENCY_LOG.filter((c) => c.procedure === \"ESD\");\n const allLog = COMPETENCY_LOG;\n\n const avg = log.length > 0\n ? log.reduce((a, c) => a + (c.grs.technical_skill + c.grs.procedural_strategy + c.grs.knowledge + c.grs.communication + c.grs.professionalism) / 5, 0) / log.length\n : 0;\n\n // Build sparkline data — chronological\n const sorted = [...log].sort((a, b) => a.date.localeCompare(b.date));\n const sparkData = sorted.map((c, i) => ({\n month: `S${i + 1}`,\n cumulative: (c.grs.technical_skill + c.grs.procedural_strategy + c.grs.knowledge + c.grs.communication + c.grs.professionalism) / 5,\n }));\n\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Hannah Liu\", subtitle: \"ST5 · Gastroenterology\", initials: \"HL\" }}>\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n My competency record\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n {allLog.length} sessions completed · longitudinal skill progression\n </p>\n </div>\n <Button variant=\"outline\">\n <Download size={13} /> Export PDF for institution\n </Button>\n </div>\n\n {/* Top stats */}\n <div className=\"grid grid-cols-1 sm:grid-cols-4 gap-3 mb-6\">\n <StatCard icon={<Activity size={16} strokeWidth={1.7} />} label=\"Sessions\" value={allLog.length.toString()} sub=\"across all procedures\" />\n <StatCard icon={<Star size={16} strokeWidth={1.7} />} label=\"Avg GRS (ESD)\" value={avg.toFixed(1)} sub=\"out of 5\" />\n <StatCard icon={<TrendingUp size={16} strokeWidth={1.7} />} label=\"Current entrustment\" value=\"Supervision\" sub=\"advanced 2 days ago\" />\n <StatCard icon={<Award size={16} strokeWidth={1.7} />} label=\"Milestones\" value={`${MILESTONES.filter((m) => m.achieved).length} / ${MILESTONES.length}`} sub=\"achieved\" />\n </div>\n\n <Tabs defaultValue=\"caselog\">\n <TabsList>\n <TabsTrigger value=\"caselog\">Case log</TabsTrigger>\n <TabsTrigger value=\"trends\">Skill trends</TabsTrigger>\n <TabsTrigger value=\"milestones\">Milestones</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"caselog\">\n <Card className=\"p-0\">\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Procedure</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Mentor</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Complexity</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>GRS</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Entrustment</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {[...allLog].sort((a, b) => b.date.localeCompare(a.date)).map((c) => {\n const avg = (c.grs.technical_skill + c.grs.procedural_strategy + c.grs.knowledge + c.grs.communication + c.grs.professionalism) / 5;\n return (\n <TableRow key={c.sessionId}>\n <TableCell className=\"px-5 py-3 text-[12px]\">{c.date}</TableCell>\n <TableCell className=\"px-5 py-3\">\n <Badge variant=\"outline\">{c.procedure}</Badge>\n </TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\">{c.mentor}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{c.complexity}</TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums font-medium\">{avg.toFixed(1)}</TableCell>\n <TableCell className=\"px-5 py-3\">\n <Badge variant={ENTRUSTMENT_VARIANT[c.entrustment]} className=\"text-[10px]\">\n {ENTRUSTMENT_LABEL[c.entrustment]}\n </Badge>\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"trends\">\n <div className=\"grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-4\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">ESD skill progression</CardTitle>\n <CardDescription>Average Global Rating Scale across {log.length} sessions.</CardDescription>\n </CardHeader>\n <CardContent>\n <UsageSparkline data={sparkData} max={5} />\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Per-domain average</CardTitle>\n <CardDescription>Identify your strengths and growth areas.</CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {GRS_DOMAINS.map((d) => {\n const domainAvg = log.reduce((a, c) => a + c.grs[d.key], 0) / Math.max(log.length, 1);\n const pct = (domainAvg / 5) * 100;\n return (\n <div key={d.key}>\n <div className=\"flex items-center justify-between mb-1.5\">\n <span className=\"text-[12px]\">{d.label}</span>\n <span className=\"text-[12px] tabular-nums font-medium\">{domainAvg.toFixed(1)}</span>\n </div>\n <div className=\"h-1.5 rounded-full overflow-hidden\" style={{ background: \"var(--color-border-default)\" }}>\n <div className=\"h-full\" style={{ width: `${pct}%`, background: \"var(--teal-500)\" }} />\n </div>\n </div>\n );\n })}\n </div>\n </CardContent>\n </Card>\n </div>\n </TabsContent>\n\n <TabsContent value=\"milestones\">\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n {MILESTONES.map((m) => {\n const Icon = m.icon;\n return (\n <Card key={m.id}>\n <CardContent className=\"pt-4 flex items-center gap-4\">\n <div className=\"w-12 h-12 rounded-xl flex items-center justify-center shrink-0\" style={{\n background: m.achieved ? \"var(--color-bg-brand-muted)\" : \"var(--color-bg-subtle)\",\n color: m.achieved ? \"var(--color-text-brand)\" : \"var(--color-text-muted)\",\n }}>\n <Icon size={22} strokeWidth={1.5} />\n </div>\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-semibold mb-0.5\" style={{ color: m.achieved ? \"var(--color-text-primary)\" : \"var(--color-text-secondary)\" }}>{m.title}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>\n {m.achieved ? `Achieved ${m.date}` : m.hint}\n </div>\n </div>\n {m.achieved && <Badge>Earned</Badge>}\n </CardContent>\n </Card>\n );\n })}\n </div>\n </TabsContent>\n </Tabs>\n </PortalShell>\n );\n}"
34
+ },
35
+ {
36
+ "id": "StatCard",
37
+ "line": 187,
38
+ "code": "function StatCard({ icon, label, value, sub }: { icon: React.ReactNode; label: string; value: string; sub: string }) {\n return (\n <Card>\n <CardContent className=\"pt-4 flex items-center gap-3\">\n <div className=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n {icon}\n </div>\n <div className=\"min-w-0\">\n <div className=\"text-[10px] font-medium uppercase tracking-wide\" style={{ color: \"var(--color-text-muted)\" }}>{label}</div>\n <div className=\"text-[20px] font-semibold tabular-nums truncate\" style={{ letterSpacing: \"-0.02em\" }}>{value}</div>\n <div className=\"text-[10px] truncate\" style={{ color: \"var(--color-text-muted)\" }}>{sub}</div>\n </div>\n </CardContent>\n </Card>\n );\n}"
39
+ }
40
+ ],
41
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useState } from \"react\";\nimport {\n Download, ArrowRight, Trophy, TrendingUp, Star, ChevronDown, Award, Sparkles, Activity,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardHeader, CardTitle, CardDescription, CardContent } from \"@/components/ui/card\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from \"@/components/ui/table\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { UsageSparkline } from \"@/components/portal/usage-sparkline\";\nimport { COMPETENCY_LOG } from \"@/lib/sessions-mock\";\nimport { ENTRUSTMENT_LABEL, GRS_DOMAINS, type EntrustmentLevel } from \"@/lib/sessions\";\n\nconst ENTRUSTMENT_VARIANT: Record<EntrustmentLevel, \"default\" | \"secondary\" | \"outline\" | \"ghost\"> = {\n observation_only: \"outline\",\n active_assistance: \"outline\",\n supervision: \"secondary\",\n independent: \"default\",\n};\n\nconst MILESTONES = [\n { id: \"first\", icon: Sparkles, title: \"First Mentored ESD\", achieved: true, date: \"2026-04-22\" },\n { id: \"5\", icon: Award, title: \"5 Sessions Completed\", achieved: false, hint: \"4 of 5\" },\n { id: \"supe\", icon: TrendingUp, title: \"Progressed to Supervision\", achieved: true, date: \"2026-05-22\" },\n { id: \"indep\", icon: Trophy, title: \"Safe for Independent Practice\", achieved: false, hint: \"Reach Independent level\" },\n];\n\nexport default function CompetencyPage() {\n const log = COMPETENCY_LOG.filter((c) => c.procedure === \"ESD\");\n const allLog = COMPETENCY_LOG;\n\n const avg = log.length > 0\n ? log.reduce((a, c) => a + (c.grs.technical_skill + c.grs.procedural_strategy + c.grs.knowledge + c.grs.communication + c.grs.professionalism) / 5, 0) / log.length\n : 0;\n\n // Build sparkline data — chronological\n const sorted = [...log].sort((a, b) => a.date.localeCompare(b.date));\n const sparkData = sorted.map((c, i) => ({\n month: `S${i + 1}`,\n cumulative: (c.grs.technical_skill + c.grs.procedural_strategy + c.grs.knowledge + c.grs.communication + c.grs.professionalism) / 5,\n }));\n\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Hannah Liu\", subtitle: \"ST5 · Gastroenterology\", initials: \"HL\" }}>\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n My competency record\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n {allLog.length} sessions completed · longitudinal skill progression\n </p>\n </div>\n <Button variant=\"outline\">\n <Download size={13} /> Export PDF for institution\n </Button>\n </div>\n\n {/* Top stats */}\n <div className=\"grid grid-cols-1 sm:grid-cols-4 gap-3 mb-6\">\n <StatCard icon={<Activity size={16} strokeWidth={1.7} />} label=\"Sessions\" value={allLog.length.toString()} sub=\"across all procedures\" />\n <StatCard icon={<Star size={16} strokeWidth={1.7} />} label=\"Avg GRS (ESD)\" value={avg.toFixed(1)} sub=\"out of 5\" />\n <StatCard icon={<TrendingUp size={16} strokeWidth={1.7} />} label=\"Current entrustment\" value=\"Supervision\" sub=\"advanced 2 days ago\" />\n <StatCard icon={<Award size={16} strokeWidth={1.7} />} label=\"Milestones\" value={`${MILESTONES.filter((m) => m.achieved).length} / ${MILESTONES.length}`} sub=\"achieved\" />\n </div>\n\n <Tabs defaultValue=\"caselog\">\n <TabsList>\n <TabsTrigger value=\"caselog\">Case log</TabsTrigger>\n <TabsTrigger value=\"trends\">Skill trends</TabsTrigger>\n <TabsTrigger value=\"milestones\">Milestones</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"caselog\">\n <Card className=\"p-0\">\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Procedure</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Mentor</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Complexity</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>GRS</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Entrustment</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {[...allLog].sort((a, b) => b.date.localeCompare(a.date)).map((c) => {\n const avg = (c.grs.technical_skill + c.grs.procedural_strategy + c.grs.knowledge + c.grs.communication + c.grs.professionalism) / 5;\n return (\n <TableRow key={c.sessionId}>\n <TableCell className=\"px-5 py-3 text-[12px]\">{c.date}</TableCell>\n <TableCell className=\"px-5 py-3\">\n <Badge variant=\"outline\">{c.procedure}</Badge>\n </TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\">{c.mentor}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{c.complexity}</TableCell>\n <TableCell className=\"px-5 py-3 text-right tabular-nums font-medium\">{avg.toFixed(1)}</TableCell>\n <TableCell className=\"px-5 py-3\">\n <Badge variant={ENTRUSTMENT_VARIANT[c.entrustment]} className=\"text-[10px]\">\n {ENTRUSTMENT_LABEL[c.entrustment]}\n </Badge>\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"trends\">\n <div className=\"grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-4\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">ESD skill progression</CardTitle>\n <CardDescription>Average Global Rating Scale across {log.length} sessions.</CardDescription>\n </CardHeader>\n <CardContent>\n <UsageSparkline data={sparkData} max={5} />\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Per-domain average</CardTitle>\n <CardDescription>Identify your strengths and growth areas.</CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {GRS_DOMAINS.map((d) => {\n const domainAvg = log.reduce((a, c) => a + c.grs[d.key], 0) / Math.max(log.length, 1);\n const pct = (domainAvg / 5) * 100;\n return (\n <div key={d.key}>\n <div className=\"flex items-center justify-between mb-1.5\">\n <span className=\"text-[12px]\">{d.label}</span>\n <span className=\"text-[12px] tabular-nums font-medium\">{domainAvg.toFixed(1)}</span>\n </div>\n <div className=\"h-1.5 rounded-full overflow-hidden\" style={{ background: \"var(--color-border-default)\" }}>\n <div className=\"h-full\" style={{ width: `${pct}%`, background: \"var(--teal-500)\" }} />\n </div>\n </div>\n );\n })}\n </div>\n </CardContent>\n </Card>\n </div>\n </TabsContent>\n\n <TabsContent value=\"milestones\">\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n {MILESTONES.map((m) => {\n const Icon = m.icon;\n return (\n <Card key={m.id}>\n <CardContent className=\"pt-4 flex items-center gap-4\">\n <div className=\"w-12 h-12 rounded-xl flex items-center justify-center shrink-0\" style={{\n background: m.achieved ? \"var(--color-bg-brand-muted)\" : \"var(--color-bg-subtle)\",\n color: m.achieved ? \"var(--color-text-brand)\" : \"var(--color-text-muted)\",\n }}>\n <Icon size={22} strokeWidth={1.5} />\n </div>\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-semibold mb-0.5\" style={{ color: m.achieved ? \"var(--color-text-primary)\" : \"var(--color-text-secondary)\" }}>{m.title}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>\n {m.achieved ? `Achieved ${m.date}` : m.hint}\n </div>\n </div>\n {m.achieved && <Badge>Earned</Badge>}\n </CardContent>\n </Card>\n );\n })}\n </div>\n </TabsContent>\n </Tabs>\n </PortalShell>\n );\n}\n\nfunction StatCard({ icon, label, value, sub }: { icon: React.ReactNode; label: string; value: string; sub: string }) {\n return (\n <Card>\n <CardContent className=\"pt-4 flex items-center gap-3\">\n <div className=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n {icon}\n </div>\n <div className=\"min-w-0\">\n <div className=\"text-[10px] font-medium uppercase tracking-wide\" style={{ color: \"var(--color-text-muted)\" }}>{label}</div>\n <div className=\"text-[20px] font-semibold tabular-nums truncate\" style={{ letterSpacing: \"-0.02em\" }}>{value}</div>\n <div className=\"text-[10px] truncate\" style={{ color: \"var(--color-text-muted)\" }}>{sub}</div>\n </div>\n </CardContent>\n </Card>\n );\n}\n"
42
+ }