@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,69 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/(auth)/onboarding/page.tsx",
4
+ "sha256": "d89210c2c0cf30cba6459ad8fa166f522fe404e0254b3c8b9dd80a88370b3703",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "next/navigation",
9
+ "lucide-react",
10
+ "@/components/ui/button",
11
+ "@/components/auth/auth-shell",
12
+ "@/components/auth/auth-primitives",
13
+ "@/components/auth/onboarding-shell",
14
+ "@/components/auth/status-indicator",
15
+ "@/lib/verification"
16
+ ],
17
+ "tokens": [
18
+ "--color-bg-brand-muted",
19
+ "--color-bg-subtle",
20
+ "--color-border-brand",
21
+ "--color-border-default",
22
+ "--color-border-muted",
23
+ "--color-status-info-bg",
24
+ "--color-status-info-text",
25
+ "--color-status-success-bg",
26
+ "--color-status-success-border",
27
+ "--color-status-success-text",
28
+ "--color-text-brand",
29
+ "--color-text-muted",
30
+ "--color-text-primary",
31
+ "--color-text-secondary",
32
+ "--gray-950",
33
+ "--green-500",
34
+ "--teal-500"
35
+ ],
36
+ "fragments": [
37
+ {
38
+ "id": "OnboardingHubPage",
39
+ "line": 26,
40
+ "code": "export default function OnboardingHubPage() {\n const search = useSearchParams();\n const roleFromUrl = search.get(\"role\") as Role | null;\n const demoState = search.get(\"demo\"); // \"fresh\" | \"midway\" | \"review\" | \"ready\" | \"complete\"\n\n const [role, setRole] = useState<Role | null>(roleFromUrl);\n\n useEffect(() => {\n if (!role) {\n try {\n const saved = localStorage.getItem(\"mentix.role\") as Role | null;\n if (saved) setRole(saved);\n } catch {}\n }\n }, [role]);\n\n // No role detected → ask user to pick one\n if (!role) return <RolePicker />;\n\n // Demo statuses for previews\n const statuses: Record<typeof STEPS[number][\"key\"], StepStatus> = (() => {\n if (demoState === \"midway\") return { profile: \"done\", credentials: \"in_progress\", legal: \"locked\", orientation: \"locked\" };\n if (demoState === \"review\") return { profile: \"done\", credentials: \"pending_review\", legal: \"locked\", orientation: \"locked\" };\n if (demoState === \"ready\") return { profile: \"done\", credentials: \"done\", legal: \"in_progress\", orientation: \"locked\" };\n if (demoState === \"complete\") return { profile: \"done\", credentials: \"done\", legal: \"done\", orientation: \"done\" };\n return { profile: \"in_progress\", credentials: \"locked\", legal: \"locked\", orientation: \"locked\" };\n })();\n\n // Map step statuses → verification status for the state machine\n const verificationStatus: VerificationStatus = (() => {\n if (statuses.orientation === \"done\") return \"active\";\n if (statuses.credentials === \"pending_review\") return \"under_verification\";\n if (statuses.legal === \"in_progress\" && role === \"mentor\") return \"legal_pending\";\n if (statuses.legal === \"in_progress\" && role === \"trainee\") return \"hardware_pending\";\n if (statuses.orientation === \"in_progress\" && role === \"trainee\") return \"credits_pending\";\n if (statuses.credentials === \"in_progress\") return \"documents_pending\";\n if (statuses.profile === \"in_progress\") return \"application_received\";\n return \"application_received\";\n })();\n\n const doneCount = Object.values(statuses).filter((s) => s === \"done\").length;\n const pct = (doneCount / STEPS.length) * 100;\n const allDone = doneCount === STEPS.length;\n\n return (\n <AuthShell\n width=\"wide\"\n kicker={`${ROLE_LABELS[role]} onboarding`}\n title=\"Set up your profile\"\n subtitle=\"Four short steps. You can come back anytime — your progress saves automatically.\"\n >\n {/* Pending-review banner if applicable */}\n {Object.values(statuses).includes(\"pending_review\") && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"info\" title=\"We're verifying your credentials\">\n Most credential reviews take under 24 hours. We'll email you the moment it's approved.\n </InlineAlert>\n </div>\n )}\n\n {allDone && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"success\" title=\"Onboarding complete\">\n Welcome to Mentix. You're all set up.\n </InlineAlert>\n </div>\n )}\n\n {/* Verification status flow */}\n <div className=\"mb-6\">\n <StatusFlow role={role} current={verificationStatus} />\n </div>\n\n {/* Credential expiry warning (mentor only, demo) */}\n {role === \"mentor\" && demoState === \"complete\" && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"warning\" title=\"Indemnity expires in 47 days\">\n Your indemnity policy expires on <strong>2026-07-12</strong>. Renew before expiry to avoid an account hold.\n </InlineAlert>\n </div>\n )}\n\n {/* Overall progress */}\n <div className=\"mb-6\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-[11px] font-semibold uppercase tracking-[0.12em]\" style={{ color: \"var(--color-text-muted)\" }}>\n Progress\n </span>\n <span className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n {doneCount} of {STEPS.length} complete\n </span>\n </div>\n <div className=\"h-1.5 rounded-full overflow-hidden\" style={{ background: \"var(--color-border-default)\" }}>\n <div className=\"h-full transition-all duration-500\" style={{ width: `${pct}%`, background: \"var(--teal-500)\" }} />\n </div>\n </div>\n\n {/* Step cards */}\n <div className=\"space-y-2\">\n {STEPS.map((s, i) => (\n <StepCard\n key={s.key}\n stepNum={i + 1}\n stepKey={s.key}\n label={s.label}\n role={role}\n status={statuses[s.key]}\n />\n ))}\n </div>\n\n {/* Bottom actions */}\n <div className=\"mt-7 pt-5 border-t flex items-center gap-2\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <Link href=\"/marketing\" className=\"flex-1\">\n <Button variant=\"outline\" size=\"lg\" className=\"w-full\">Save & exit</Button>\n </Link>\n {!allDone && (\n <ContinueButton role={role} statuses={statuses} />\n )}\n {allDone && (\n <Link href=\"/marketing\" className=\"flex-1\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">\n Enter Mentix <ArrowRight size={14} />\n </Button>\n </Link>\n )}\n </div>\n </AuthShell>\n );\n}"
41
+ },
42
+ {
43
+ "id": "StepCard",
44
+ "line": 159,
45
+ "code": "function StepCard({ stepNum, stepKey, label, role, status }: {\n stepNum: number;\n stepKey: typeof STEPS[number][\"key\"];\n label: string;\n role: Role;\n status: StepStatus;\n}) {\n const meta = STEP_META[stepKey];\n const Icon = meta.icon;\n const isLocked = status === \"locked\";\n const isDone = status === \"done\";\n const isReview = status === \"pending_review\";\n const inProgress = status === \"in_progress\";\n\n const href = `/onboarding/${stepKey}?role=${role}`;\n\n const card = (\n <div\n className=\"rounded-2xl border p-4 flex items-start gap-3.5 transition-all\"\n style={{\n borderColor: inProgress\n ? \"var(--color-border-brand)\"\n : isDone\n ? \"var(--color-status-success-border)\"\n : \"var(--color-border-default)\",\n background: inProgress\n ? \"var(--color-bg-brand-muted)\"\n : isDone\n ? \"var(--color-status-success-bg)\"\n : \"transparent\",\n opacity: isLocked ? 0.55 : 1,\n }}\n >\n <div\n className=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n style={{\n background: isDone\n ? \"var(--green-500)\"\n : inProgress\n ? \"var(--teal-500)\"\n : \"var(--color-bg-subtle)\",\n color: isDone || inProgress ? \"var(--gray-950)\" : \"var(--color-text-secondary)\",\n }}\n >\n {isDone ? <CheckCircle2 size={18} strokeWidth={2} /> : <Icon size={18} strokeWidth={1.8} />}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-0.5\">\n <span className=\"text-[11px] font-semibold tabular-nums\" style={{ color: \"var(--color-text-muted)\" }}>\n Step {stepNum}\n </span>\n <span className=\"text-[15px] font-semibold\" style={{ color: \"var(--color-text-primary)\" }}>\n {label}\n </span>\n <StatusPill status={status} />\n </div>\n <p className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{meta.desc}</p>\n </div>\n <div className=\"shrink-0 self-center\">\n {isLocked ? (\n <Lock size={15} strokeWidth={1.8} style={{ color: \"var(--color-text-muted)\" }} />\n ) : isReview ? (\n <span className=\"text-[11px] font-medium px-2 py-1 rounded-md\" style={{ background: \"var(--color-status-info-bg)\", color: \"var(--color-status-info-text)\" }}>\n Reviewing\n </span>\n ) : (\n <ArrowRight size={15} strokeWidth={1.8} style={{ color: \"var(--color-text-secondary)\" }} />\n )}\n </div>\n </div>\n );\n\n if (isLocked || isReview) {\n return <div aria-disabled>{card}</div>;\n }\n return (\n <Link href={href} className=\"block transition-shadow hover:shadow-sm rounded-2xl\">\n {card}\n </Link>\n );\n}"
46
+ },
47
+ {
48
+ "id": "StatusPill",
49
+ "line": 241,
50
+ "code": "function StatusPill({ status }: { status: StepStatus }) {\n if (status === \"done\") return <Pill color=\"success\">Done</Pill>;\n if (status === \"in_progress\") return <Pill color=\"brand\">In progress</Pill>;\n if (status === \"pending_review\") return <Pill color=\"info\">Awaiting review</Pill>;\n return null;\n}"
51
+ },
52
+ {
53
+ "id": "Pill",
54
+ "line": 247,
55
+ "code": "function Pill({ color, children }: { color: \"success\" | \"brand\" | \"info\"; children: React.ReactNode }) {\n const bg =\n color === \"success\" ? \"color-mix(in srgb, var(--green-500) 18%, transparent)\" :\n color === \"info\" ? \"var(--color-status-info-bg)\" :\n \"var(--color-bg-brand-muted)\";\n const fg =\n color === \"success\" ? \"var(--color-status-success-text)\" :\n color === \"info\" ? \"var(--color-status-info-text)\" :\n \"var(--color-text-brand)\";\n return (\n <span className=\"text-[10px] font-medium px-1.5 py-0.5 rounded-md uppercase tracking-wide\" style={{ background: bg, color: fg }}>\n {children}\n </span>\n );\n}"
56
+ },
57
+ {
58
+ "id": "ContinueButton",
59
+ "line": 265,
60
+ "code": "function ContinueButton({ role, statuses }: { role: Role; statuses: Record<typeof STEPS[number][\"key\"], StepStatus> }) {\n const next = STEPS.find((s) => statuses[s.key] === \"in_progress\" || statuses[s.key] === \"todo\")\n ?? STEPS.find((s) => statuses[s.key] !== \"done\" && statuses[s.key] !== \"pending_review\");\n if (!next) return null;\n return (\n <Link href={`/onboarding/${next.key}?role=${role}`} className=\"flex-1\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">\n Continue <ArrowRight size={14} />\n </Button>\n </Link>\n );\n}"
61
+ },
62
+ {
63
+ "id": "RolePicker",
64
+ "line": 280,
65
+ "code": "function RolePicker() {\n const items: { id: Role; icon: typeof GraduationCap; title: string; desc: string }[] = [\n { id: \"trainee\", icon: GraduationCap, title: \"Trainee\", desc: \"Request mentored sessions and build competency.\" },\n { id: \"mentor\", icon: Stethoscope, title: \"Mentor\", desc: \"Deliver mentorship and review session requests.\" },\n { id: \"sponsor\", icon: Building2, title: \"Sponsor\", desc: \"Allocate credits and track group outcomes.\" },\n ];\n const pick = (r: Role) => {\n try { localStorage.setItem(\"mentix.role\", r); } catch {}\n window.location.href = `/onboarding?role=${r}`;\n };\n return (\n <AuthShell title=\"How will you use Mentix?\" subtitle=\"Pick one to begin onboarding. You can contact support to change this later.\">\n <div className=\"space-y-2.5\">\n {items.map((r) => {\n const Icon = r.icon;\n return (\n <button\n key={r.id}\n type=\"button\"\n onClick={() => pick(r.id)}\n className=\"w-full text-left rounded-2xl border p-5 transition-all hover:bg-[var(--color-bg-subtle)] focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n style={{ borderColor: \"var(--color-border-default)\" }}\n >\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <Icon size={18} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1\">\n <div className=\"text-[15px] font-semibold mb-1\">{r.title}</div>\n <div className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>{r.desc}</div>\n </div>\n <ArrowRight size={16} strokeWidth={1.8} style={{ color: \"var(--color-text-muted)\" }} className=\"mt-1\" />\n </div>\n </button>\n );\n })}\n </div>\n </AuthShell>\n );\n}"
66
+ }
67
+ ],
68
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useState, useEffect } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport {\n ArrowRight, CheckCircle2, Lock, User, BadgeCheck, FileText, GraduationCap,\n Stethoscope, Building2,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport { InlineAlert } from \"@/components/auth/auth-primitives\";\nimport { Role, STEPS, ROLE_LABELS } from \"@/components/auth/onboarding-shell\";\nimport { StatusFlow } from \"@/components/auth/status-indicator\";\nimport type { VerificationStatus } from \"@/lib/verification\";\n\nconst STEP_META: Record<typeof STEPS[number][\"key\"], { icon: typeof User; desc: string }> = {\n profile: { icon: User, desc: \"Your name, photo, language, time zone.\" },\n credentials: { icon: BadgeCheck, desc: \"Professional information for verification.\" },\n legal: { icon: FileText, desc: \"Sign agreements required to proceed.\" },\n orientation: { icon: GraduationCap, desc: \"Quick platform walkthrough.\" },\n};\n\ntype StepStatus = \"todo\" | \"in_progress\" | \"done\" | \"locked\" | \"pending_review\";\n\nexport default function OnboardingHubPage() {\n const search = useSearchParams();\n const roleFromUrl = search.get(\"role\") as Role | null;\n const demoState = search.get(\"demo\"); // \"fresh\" | \"midway\" | \"review\" | \"ready\" | \"complete\"\n\n const [role, setRole] = useState<Role | null>(roleFromUrl);\n\n useEffect(() => {\n if (!role) {\n try {\n const saved = localStorage.getItem(\"mentix.role\") as Role | null;\n if (saved) setRole(saved);\n } catch {}\n }\n }, [role]);\n\n // No role detected → ask user to pick one\n if (!role) return <RolePicker />;\n\n // Demo statuses for previews\n const statuses: Record<typeof STEPS[number][\"key\"], StepStatus> = (() => {\n if (demoState === \"midway\") return { profile: \"done\", credentials: \"in_progress\", legal: \"locked\", orientation: \"locked\" };\n if (demoState === \"review\") return { profile: \"done\", credentials: \"pending_review\", legal: \"locked\", orientation: \"locked\" };\n if (demoState === \"ready\") return { profile: \"done\", credentials: \"done\", legal: \"in_progress\", orientation: \"locked\" };\n if (demoState === \"complete\") return { profile: \"done\", credentials: \"done\", legal: \"done\", orientation: \"done\" };\n return { profile: \"in_progress\", credentials: \"locked\", legal: \"locked\", orientation: \"locked\" };\n })();\n\n // Map step statuses → verification status for the state machine\n const verificationStatus: VerificationStatus = (() => {\n if (statuses.orientation === \"done\") return \"active\";\n if (statuses.credentials === \"pending_review\") return \"under_verification\";\n if (statuses.legal === \"in_progress\" && role === \"mentor\") return \"legal_pending\";\n if (statuses.legal === \"in_progress\" && role === \"trainee\") return \"hardware_pending\";\n if (statuses.orientation === \"in_progress\" && role === \"trainee\") return \"credits_pending\";\n if (statuses.credentials === \"in_progress\") return \"documents_pending\";\n if (statuses.profile === \"in_progress\") return \"application_received\";\n return \"application_received\";\n })();\n\n const doneCount = Object.values(statuses).filter((s) => s === \"done\").length;\n const pct = (doneCount / STEPS.length) * 100;\n const allDone = doneCount === STEPS.length;\n\n return (\n <AuthShell\n width=\"wide\"\n kicker={`${ROLE_LABELS[role]} onboarding`}\n title=\"Set up your profile\"\n subtitle=\"Four short steps. You can come back anytime — your progress saves automatically.\"\n >\n {/* Pending-review banner if applicable */}\n {Object.values(statuses).includes(\"pending_review\") && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"info\" title=\"We're verifying your credentials\">\n Most credential reviews take under 24 hours. We'll email you the moment it's approved.\n </InlineAlert>\n </div>\n )}\n\n {allDone && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"success\" title=\"Onboarding complete\">\n Welcome to Mentix. You're all set up.\n </InlineAlert>\n </div>\n )}\n\n {/* Verification status flow */}\n <div className=\"mb-6\">\n <StatusFlow role={role} current={verificationStatus} />\n </div>\n\n {/* Credential expiry warning (mentor only, demo) */}\n {role === \"mentor\" && demoState === \"complete\" && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"warning\" title=\"Indemnity expires in 47 days\">\n Your indemnity policy expires on <strong>2026-07-12</strong>. Renew before expiry to avoid an account hold.\n </InlineAlert>\n </div>\n )}\n\n {/* Overall progress */}\n <div className=\"mb-6\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-[11px] font-semibold uppercase tracking-[0.12em]\" style={{ color: \"var(--color-text-muted)\" }}>\n Progress\n </span>\n <span className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n {doneCount} of {STEPS.length} complete\n </span>\n </div>\n <div className=\"h-1.5 rounded-full overflow-hidden\" style={{ background: \"var(--color-border-default)\" }}>\n <div className=\"h-full transition-all duration-500\" style={{ width: `${pct}%`, background: \"var(--teal-500)\" }} />\n </div>\n </div>\n\n {/* Step cards */}\n <div className=\"space-y-2\">\n {STEPS.map((s, i) => (\n <StepCard\n key={s.key}\n stepNum={i + 1}\n stepKey={s.key}\n label={s.label}\n role={role}\n status={statuses[s.key]}\n />\n ))}\n </div>\n\n {/* Bottom actions */}\n <div className=\"mt-7 pt-5 border-t flex items-center gap-2\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <Link href=\"/marketing\" className=\"flex-1\">\n <Button variant=\"outline\" size=\"lg\" className=\"w-full\">Save & exit</Button>\n </Link>\n {!allDone && (\n <ContinueButton role={role} statuses={statuses} />\n )}\n {allDone && (\n <Link href=\"/marketing\" className=\"flex-1\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">\n Enter Mentix <ArrowRight size={14} />\n </Button>\n </Link>\n )}\n </div>\n </AuthShell>\n );\n}\n\n/* ─── Step card ─────────────────────────────────────────────────────────── */\n\nfunction StepCard({ stepNum, stepKey, label, role, status }: {\n stepNum: number;\n stepKey: typeof STEPS[number][\"key\"];\n label: string;\n role: Role;\n status: StepStatus;\n}) {\n const meta = STEP_META[stepKey];\n const Icon = meta.icon;\n const isLocked = status === \"locked\";\n const isDone = status === \"done\";\n const isReview = status === \"pending_review\";\n const inProgress = status === \"in_progress\";\n\n const href = `/onboarding/${stepKey}?role=${role}`;\n\n const card = (\n <div\n className=\"rounded-2xl border p-4 flex items-start gap-3.5 transition-all\"\n style={{\n borderColor: inProgress\n ? \"var(--color-border-brand)\"\n : isDone\n ? \"var(--color-status-success-border)\"\n : \"var(--color-border-default)\",\n background: inProgress\n ? \"var(--color-bg-brand-muted)\"\n : isDone\n ? \"var(--color-status-success-bg)\"\n : \"transparent\",\n opacity: isLocked ? 0.55 : 1,\n }}\n >\n <div\n className=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n style={{\n background: isDone\n ? \"var(--green-500)\"\n : inProgress\n ? \"var(--teal-500)\"\n : \"var(--color-bg-subtle)\",\n color: isDone || inProgress ? \"var(--gray-950)\" : \"var(--color-text-secondary)\",\n }}\n >\n {isDone ? <CheckCircle2 size={18} strokeWidth={2} /> : <Icon size={18} strokeWidth={1.8} />}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-0.5\">\n <span className=\"text-[11px] font-semibold tabular-nums\" style={{ color: \"var(--color-text-muted)\" }}>\n Step {stepNum}\n </span>\n <span className=\"text-[15px] font-semibold\" style={{ color: \"var(--color-text-primary)\" }}>\n {label}\n </span>\n <StatusPill status={status} />\n </div>\n <p className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{meta.desc}</p>\n </div>\n <div className=\"shrink-0 self-center\">\n {isLocked ? (\n <Lock size={15} strokeWidth={1.8} style={{ color: \"var(--color-text-muted)\" }} />\n ) : isReview ? (\n <span className=\"text-[11px] font-medium px-2 py-1 rounded-md\" style={{ background: \"var(--color-status-info-bg)\", color: \"var(--color-status-info-text)\" }}>\n Reviewing\n </span>\n ) : (\n <ArrowRight size={15} strokeWidth={1.8} style={{ color: \"var(--color-text-secondary)\" }} />\n )}\n </div>\n </div>\n );\n\n if (isLocked || isReview) {\n return <div aria-disabled>{card}</div>;\n }\n return (\n <Link href={href} className=\"block transition-shadow hover:shadow-sm rounded-2xl\">\n {card}\n </Link>\n );\n}\n\nfunction StatusPill({ status }: { status: StepStatus }) {\n if (status === \"done\") return <Pill color=\"success\">Done</Pill>;\n if (status === \"in_progress\") return <Pill color=\"brand\">In progress</Pill>;\n if (status === \"pending_review\") return <Pill color=\"info\">Awaiting review</Pill>;\n return null;\n}\nfunction Pill({ color, children }: { color: \"success\" | \"brand\" | \"info\"; children: React.ReactNode }) {\n const bg =\n color === \"success\" ? \"color-mix(in srgb, var(--green-500) 18%, transparent)\" :\n color === \"info\" ? \"var(--color-status-info-bg)\" :\n \"var(--color-bg-brand-muted)\";\n const fg =\n color === \"success\" ? \"var(--color-status-success-text)\" :\n color === \"info\" ? \"var(--color-status-info-text)\" :\n \"var(--color-text-brand)\";\n return (\n <span className=\"text-[10px] font-medium px-1.5 py-0.5 rounded-md uppercase tracking-wide\" style={{ background: bg, color: fg }}>\n {children}\n </span>\n );\n}\n\n/* ─── Continue button (sends to next pending step) ───────────────────────── */\n\nfunction ContinueButton({ role, statuses }: { role: Role; statuses: Record<typeof STEPS[number][\"key\"], StepStatus> }) {\n const next = STEPS.find((s) => statuses[s.key] === \"in_progress\" || statuses[s.key] === \"todo\")\n ?? STEPS.find((s) => statuses[s.key] !== \"done\" && statuses[s.key] !== \"pending_review\");\n if (!next) return null;\n return (\n <Link href={`/onboarding/${next.key}?role=${role}`} className=\"flex-1\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">\n Continue <ArrowRight size={14} />\n </Button>\n </Link>\n );\n}\n\n/* ─── Role picker (no saved role) ────────────────────────────────────────── */\n\nfunction RolePicker() {\n const items: { id: Role; icon: typeof GraduationCap; title: string; desc: string }[] = [\n { id: \"trainee\", icon: GraduationCap, title: \"Trainee\", desc: \"Request mentored sessions and build competency.\" },\n { id: \"mentor\", icon: Stethoscope, title: \"Mentor\", desc: \"Deliver mentorship and review session requests.\" },\n { id: \"sponsor\", icon: Building2, title: \"Sponsor\", desc: \"Allocate credits and track group outcomes.\" },\n ];\n const pick = (r: Role) => {\n try { localStorage.setItem(\"mentix.role\", r); } catch {}\n window.location.href = `/onboarding?role=${r}`;\n };\n return (\n <AuthShell title=\"How will you use Mentix?\" subtitle=\"Pick one to begin onboarding. You can contact support to change this later.\">\n <div className=\"space-y-2.5\">\n {items.map((r) => {\n const Icon = r.icon;\n return (\n <button\n key={r.id}\n type=\"button\"\n onClick={() => pick(r.id)}\n className=\"w-full text-left rounded-2xl border p-5 transition-all hover:bg-[var(--color-bg-subtle)] focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n style={{ borderColor: \"var(--color-border-default)\" }}\n >\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <Icon size={18} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1\">\n <div className=\"text-[15px] font-semibold mb-1\">{r.title}</div>\n <div className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>{r.desc}</div>\n </div>\n <ArrowRight size={16} strokeWidth={1.8} style={{ color: \"var(--color-text-muted)\" }} className=\"mt-1\" />\n </div>\n </button>\n );\n })}\n </div>\n </AuthShell>\n );\n}\n"
69
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/(auth)/reset-password/page.tsx",
4
+ "sha256": "ed8843046a5966764f0ab03d2513596328471df52eef3fc4a61a2fda927d04af",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "next/navigation",
9
+ "lucide-react",
10
+ "@/components/ui/button",
11
+ "@/components/auth/auth-shell",
12
+ "@/components/auth/auth-primitives"
13
+ ],
14
+ "tokens": [
15
+ "--color-status-success-bg",
16
+ "--color-status-success-text",
17
+ "--color-text-secondary"
18
+ ],
19
+ "fragments": [
20
+ {
21
+ "id": "ResetPasswordPage",
22
+ "line": 14,
23
+ "code": "export default function ResetPasswordPage() {\n const search = useSearchParams();\n const forced = search.get(\"state\") as State | null;\n\n const [password, setPassword] = useState(\"\");\n const [confirm, setConfirm] = useState(\"\");\n const [autoSignIn, setAutoSignIn] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [success, setSuccess] = useState(false);\n const [mismatch, setMismatch] = useState(false);\n\n const { score } = passwordStrength(password);\n const state: State = useMemo(() => {\n if (forced) return forced;\n if (submitting) return \"loading\";\n if (success) return \"success\";\n if (mismatch) return \"mismatch\";\n return \"idle\";\n }, [forced, submitting, success, mismatch]);\n\n const canSubmit = score >= 2 && password === confirm && password.length >= 8;\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (password !== confirm) { setMismatch(true); return; }\n setMismatch(false);\n setSubmitting(true);\n setTimeout(() => {\n setSubmitting(false);\n setSuccess(true);\n if (autoSignIn) setTimeout(() => window.location.href = \"/welcome\", 1500);\n }, 1200);\n };\n\n // ─── Expired ──\n if (state === \"expired\") {\n return (\n <AuthShell kicker=\"Link expired\" title=\"This reset link has expired\" subtitle=\"Reset links are valid for 60 minutes. Request a fresh one to continue.\">\n <div className=\"space-y-4\">\n <InlineAlert variant=\"warning\">For your security, password reset tokens cannot be reused after they expire.</InlineAlert>\n <Link href=\"/forgot-password\" className=\"block\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">Request new reset link <ArrowRight size={14} /></Button>\n </Link>\n </div>\n </AuthShell>\n );\n }\n\n // ─── Success ──\n if (state === \"success\") {\n return (\n <AuthShell kicker=\"All set\" title=\"Password updated\" subtitle={autoSignIn ? \"Signing you in now…\" : \"You can now sign in with your new password.\"}>\n <div className=\"flex flex-col items-center text-center py-4\">\n <div className=\"w-16 h-16 rounded-full flex items-center justify-center mb-5\"\n style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}>\n <CheckCircle2 size={32} strokeWidth={1.6} />\n </div>\n {autoSignIn ? (\n <p className=\"text-sm flex items-center gap-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Spinner className=\"w-4 h-4\" /> Redirecting to your workspace…\n </p>\n ) : (\n <Link href=\"/login\" className=\"w-full\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">Sign in <ArrowRight size={14} /></Button>\n </Link>\n )}\n </div>\n </AuthShell>\n );\n }\n\n // ─── Idle / Loading ──\n return (\n <AuthShell\n kicker=\"Set new password\"\n title=\"Choose a new password\"\n subtitle=\"Pick something secure. We recommend using a password manager.\"\n >\n {state === \"mismatch\" && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"error\" title=\"Passwords don't match\">\n The two passwords you entered are different. Please try again.\n </InlineAlert>\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\" noValidate>\n <Field label=\"New password\" htmlFor=\"pw\">\n <PasswordInput id=\"pw\" value={password} onChange={setPassword} placeholder=\"At least 8 characters\" autoComplete=\"new-password\" />\n {password && <PasswordStrength password={password} />}\n </Field>\n\n <Field\n label=\"Confirm new password\"\n htmlFor=\"confirm\"\n error={confirm && password !== confirm ? \"Passwords don't match\" : undefined}\n >\n <PasswordInput id=\"confirm\" value={confirm} onChange={(v) => { setConfirm(v); setMismatch(false); }} placeholder=\"Re-enter the password\" autoComplete=\"new-password\" ariaInvalid={!!confirm && password !== confirm} />\n </Field>\n\n <Checkbox id=\"autosignin\" checked={autoSignIn} onChange={setAutoSignIn}>\n Sign me in automatically after resetting\n </Checkbox>\n\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" disabled={!canSubmit || submitting} className=\"mt-2\">\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Updating password…</> : <>Update password <ArrowRight size={14} /></>}\n </Button>\n </form>\n </AuthShell>\n );\n}"
24
+ }
25
+ ],
26
+ "content": "\"use client\";\nimport Link from \"next/link\";\nimport { useState, useMemo } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowRight, CheckCircle2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport {\n Field, PasswordInput, PasswordStrength, InlineAlert, Checkbox, Spinner, passwordStrength,\n} from \"@/components/auth/auth-primitives\";\n\ntype State = \"idle\" | \"loading\" | \"success\" | \"expired\" | \"mismatch\";\n\nexport default function ResetPasswordPage() {\n const search = useSearchParams();\n const forced = search.get(\"state\") as State | null;\n\n const [password, setPassword] = useState(\"\");\n const [confirm, setConfirm] = useState(\"\");\n const [autoSignIn, setAutoSignIn] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [success, setSuccess] = useState(false);\n const [mismatch, setMismatch] = useState(false);\n\n const { score } = passwordStrength(password);\n const state: State = useMemo(() => {\n if (forced) return forced;\n if (submitting) return \"loading\";\n if (success) return \"success\";\n if (mismatch) return \"mismatch\";\n return \"idle\";\n }, [forced, submitting, success, mismatch]);\n\n const canSubmit = score >= 2 && password === confirm && password.length >= 8;\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (password !== confirm) { setMismatch(true); return; }\n setMismatch(false);\n setSubmitting(true);\n setTimeout(() => {\n setSubmitting(false);\n setSuccess(true);\n if (autoSignIn) setTimeout(() => window.location.href = \"/welcome\", 1500);\n }, 1200);\n };\n\n // ─── Expired ──\n if (state === \"expired\") {\n return (\n <AuthShell kicker=\"Link expired\" title=\"This reset link has expired\" subtitle=\"Reset links are valid for 60 minutes. Request a fresh one to continue.\">\n <div className=\"space-y-4\">\n <InlineAlert variant=\"warning\">For your security, password reset tokens cannot be reused after they expire.</InlineAlert>\n <Link href=\"/forgot-password\" className=\"block\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">Request new reset link <ArrowRight size={14} /></Button>\n </Link>\n </div>\n </AuthShell>\n );\n }\n\n // ─── Success ──\n if (state === \"success\") {\n return (\n <AuthShell kicker=\"All set\" title=\"Password updated\" subtitle={autoSignIn ? \"Signing you in now…\" : \"You can now sign in with your new password.\"}>\n <div className=\"flex flex-col items-center text-center py-4\">\n <div className=\"w-16 h-16 rounded-full flex items-center justify-center mb-5\"\n style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}>\n <CheckCircle2 size={32} strokeWidth={1.6} />\n </div>\n {autoSignIn ? (\n <p className=\"text-sm flex items-center gap-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Spinner className=\"w-4 h-4\" /> Redirecting to your workspace…\n </p>\n ) : (\n <Link href=\"/login\" className=\"w-full\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">Sign in <ArrowRight size={14} /></Button>\n </Link>\n )}\n </div>\n </AuthShell>\n );\n }\n\n // ─── Idle / Loading ──\n return (\n <AuthShell\n kicker=\"Set new password\"\n title=\"Choose a new password\"\n subtitle=\"Pick something secure. We recommend using a password manager.\"\n >\n {state === \"mismatch\" && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"error\" title=\"Passwords don't match\">\n The two passwords you entered are different. Please try again.\n </InlineAlert>\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\" noValidate>\n <Field label=\"New password\" htmlFor=\"pw\">\n <PasswordInput id=\"pw\" value={password} onChange={setPassword} placeholder=\"At least 8 characters\" autoComplete=\"new-password\" />\n {password && <PasswordStrength password={password} />}\n </Field>\n\n <Field\n label=\"Confirm new password\"\n htmlFor=\"confirm\"\n error={confirm && password !== confirm ? \"Passwords don't match\" : undefined}\n >\n <PasswordInput id=\"confirm\" value={confirm} onChange={(v) => { setConfirm(v); setMismatch(false); }} placeholder=\"Re-enter the password\" autoComplete=\"new-password\" ariaInvalid={!!confirm && password !== confirm} />\n </Field>\n\n <Checkbox id=\"autosignin\" checked={autoSignIn} onChange={setAutoSignIn}>\n Sign me in automatically after resetting\n </Checkbox>\n\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" disabled={!canSubmit || submitting} className=\"mt-2\">\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Updating password…</> : <>Update password <ArrowRight size={14} /></>}\n </Button>\n </form>\n </AuthShell>\n );\n}\n"
27
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/(auth)/session-expired/page.tsx",
4
+ "sha256": "475dcc7eb6f8603f4de04db5923823247ab66be85e8582e3771aabd1ddfa7bbe",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "next/navigation",
9
+ "lucide-react",
10
+ "@/components/ui/button",
11
+ "@/components/ui/input",
12
+ "@/components/auth/auth-shell",
13
+ "@/components/auth/auth-primitives"
14
+ ],
15
+ "tokens": [
16
+ "--color-bg-brand-muted",
17
+ "--color-bg-default",
18
+ "--color-bg-subtle",
19
+ "--color-bg-ui",
20
+ "--color-border-default",
21
+ "--color-border-muted",
22
+ "--color-status-error-text",
23
+ "--color-text-brand",
24
+ "--color-text-muted",
25
+ "--color-text-primary",
26
+ "--color-text-secondary",
27
+ "--gray-950"
28
+ ],
29
+ "fragments": [
30
+ {
31
+ "id": "SessionExpiredPage",
32
+ "line": 13,
33
+ "code": "export default function SessionExpiredPage() {\n const search = useSearchParams();\n const stateParam = (search.get(\"state\") as State | null) ?? \"expired-modal\";\n\n if (stateParam === \"logged-out\") return <LoggedOutView />;\n if (stateParam === \"concurrent\") return <ConcurrentSessionView />;\n if (stateParam === \"expired-full\") return <ExpiredFullView />;\n return <ExpiredModalView />;\n}"
34
+ },
35
+ {
36
+ "id": "ExpiredModalView",
37
+ "line": 25,
38
+ "code": "function ExpiredModalView() {\n const [pw, setPw] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n\n return (\n <div className=\"min-h-screen relative\" style={{ background: \"var(--color-bg-default)\" }}>\n {/* Dimmed pseudo-app behind modal */}\n <FakeAppBackdrop />\n\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\" style={{ background: \"color-mix(in srgb, var(--gray-950) 70%, transparent)\", backdropFilter: \"blur(2px)\" }}>\n <div className=\"w-full max-w-md rounded-2xl p-6 shadow-2xl\" style={{ background: \"var(--color-bg-default)\", border: \"1px solid var(--color-border-default)\" }}>\n <div className=\"flex items-start gap-3 mb-5\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Clock size={18} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1\">\n <h2 className=\"text-base font-semibold mb-1\">Your session expired</h2>\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\", lineHeight: 1.55 }}>\n For your security, we signed you out after a period of inactivity. Sign back in to continue where you left off.\n </p>\n </div>\n </div>\n\n <form onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => window.location.href = \"/welcome\", 1000); }} className=\"space-y-3\">\n <Field label=\"Password\" htmlFor=\"pw\">\n <PasswordInput id=\"pw\" value={pw} onChange={setPw} autoComplete=\"current-password\" />\n </Field>\n <div className=\"flex gap-2 pt-1\">\n <Link href=\"/login\" className=\"flex-1\">\n <Button type=\"button\" variant=\"outline\" className=\"w-full\">Sign in fresh</Button>\n </Link>\n <Button type=\"submit\" variant=\"brand\" disabled={pw.length < 4 || submitting} className=\"flex-1\">\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Restoring…</> : \"Continue\"}\n </Button>\n </div>\n </form>\n\n <p className=\"text-[11px] mt-4 pt-4 border-t text-center\" style={{ borderColor: \"var(--color-border-muted)\", color: \"var(--color-text-muted)\" }}>\n Signed in as <strong style={{ color: \"var(--color-text-primary)\" }}>sarah.park@kcl.ac.uk</strong>\n </p>\n </div>\n </div>\n </div>\n );\n}"
39
+ },
40
+ {
41
+ "id": "FakeAppBackdrop",
42
+ "line": 71,
43
+ "code": "function FakeAppBackdrop() {\n return (\n <div className=\"absolute inset-0 overflow-hidden\" aria-hidden>\n <div className=\"h-14 border-b px-6 flex items-center gap-3\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-6 h-6 rounded\" style={{ background: \"var(--color-bg-ui)\" }} />\n <div className=\"w-24 h-3 rounded\" style={{ background: \"var(--color-bg-ui)\" }} />\n </div>\n <div className=\"grid grid-cols-12 gap-6 p-8\">\n <div className=\"col-span-3 space-y-3\">\n {[1,2,3,4].map(i => <div key={i} className=\"h-8 rounded\" style={{ background: \"var(--color-bg-subtle)\" }} />)}\n </div>\n <div className=\"col-span-9 space-y-4\">\n <div className=\"h-8 w-1/3 rounded\" style={{ background: \"var(--color-bg-subtle)\" }} />\n <div className=\"grid grid-cols-3 gap-4\">\n {[1,2,3].map(i => <div key={i} className=\"h-32 rounded-xl\" style={{ background: \"var(--color-bg-subtle)\" }} />)}\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n {[1,2].map(i => <div key={i} className=\"h-48 rounded-xl\" style={{ background: \"var(--color-bg-subtle)\" }} />)}\n </div>\n </div>\n </div>\n </div>\n );\n}"
44
+ },
45
+ {
46
+ "id": "ExpiredFullView",
47
+ "line": 98,
48
+ "code": "function ExpiredFullView() {\n return (\n <AuthShell kicker=\"Welcome back\" title=\"Your session has expired\" subtitle=\"It's been a while since you were last here. Sign in to continue your work.\">\n <div className=\"space-y-4\">\n <InlineAlert variant=\"info\">\n We sign users out after 30 days of inactivity to keep accounts secure.\n </InlineAlert>\n <Link href=\"/login\"><Button variant=\"brand\" size=\"lg\" className=\"w-full\">Sign back in <ArrowRight size={14} /></Button></Link>\n </div>\n </AuthShell>\n );\n}"
49
+ },
50
+ {
51
+ "id": "LoggedOutView",
52
+ "line": 113,
53
+ "code": "function LoggedOutView() {\n const [seconds, setSeconds] = useState(5);\n useEffect(() => {\n if (seconds > 0) {\n const t = setTimeout(() => setSeconds(s => s - 1), 1000);\n return () => clearTimeout(t);\n } else {\n window.location.href = \"/marketing\";\n }\n }, [seconds]);\n\n return (\n <AuthShell kicker=\"See you soon\" title=\"You're signed out\" subtitle=\"Your session ended. Redirecting you to the homepage…\">\n <div className=\"flex flex-col items-center text-center py-4 space-y-5\">\n <div className=\"w-16 h-16 rounded-full flex items-center justify-center\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <LogOut size={28} strokeWidth={1.6} />\n </div>\n <p className=\"text-sm flex items-center gap-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Spinner className=\"w-4 h-4\" /> Redirecting in {seconds}s…\n </p>\n <div className=\"flex flex-col gap-2 w-full pt-2\">\n <Link href=\"/login\"><Button variant=\"brand\" size=\"lg\" className=\"w-full\">Sign in again</Button></Link>\n <Link href=\"/marketing\"><Button variant=\"outline\" size=\"lg\" className=\"w-full\">Go to homepage</Button></Link>\n </div>\n </div>\n </AuthShell>\n );\n}"
54
+ },
55
+ {
56
+ "id": "ConcurrentSessionView",
57
+ "line": 144,
58
+ "code": "function ConcurrentSessionView() {\n return (\n <AuthShell kicker=\"Active session detected\" title=\"You're signed in elsewhere\" subtitle=\"Your account is currently active on another device. For security, only one active session is allowed at a time.\">\n <div className=\"space-y-4\">\n <div className=\"rounded-2xl border\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"p-4 flex items-start gap-3\" style={{ borderBottom: \"1px solid var(--color-border-muted)\" }}>\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <MonitorSmartphone size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium flex items-center gap-2\">\n Chrome on MacBook Pro\n <span className=\"text-[10px] font-medium px-1.5 py-0.5 rounded-md\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>Current</span>\n </div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>London, UK · Active 2 minutes ago</div>\n </div>\n </div>\n <div className=\"p-4 flex items-start gap-3\">\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <MonitorSmartphone size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">Safari on iPhone</div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>London, UK · Active just now</div>\n </div>\n </div>\n </div>\n\n <div className=\"flex flex-col gap-2 pt-1\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">Sign out other devices & continue here <ArrowRight size={14} /></Button>\n <Link href=\"/login\"><Button variant=\"outline\" size=\"lg\" className=\"w-full\">Cancel and sign out</Button></Link>\n </div>\n\n <p className=\"text-[11px] text-center pt-2\" style={{ color: \"var(--color-text-muted)\" }}>\n Don't recognize the other device?{\" \"}\n <Link href=\"/locked?state=suspicious\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-status-error-text)\" }}>\n Secure your account\n </Link>.\n </p>\n </div>\n </AuthShell>\n );\n}"
59
+ }
60
+ ],
61
+ "content": "\"use client\";\nimport Link from \"next/link\";\nimport { useState, useEffect } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowRight, Clock, LogOut, MonitorSmartphone } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport { Field, PasswordInput, InlineAlert, Spinner } from \"@/components/auth/auth-primitives\";\n\ntype State = \"expired-modal\" | \"expired-full\" | \"logged-out\" | \"concurrent\";\n\nexport default function SessionExpiredPage() {\n const search = useSearchParams();\n const stateParam = (search.get(\"state\") as State | null) ?? \"expired-modal\";\n\n if (stateParam === \"logged-out\") return <LoggedOutView />;\n if (stateParam === \"concurrent\") return <ConcurrentSessionView />;\n if (stateParam === \"expired-full\") return <ExpiredFullView />;\n return <ExpiredModalView />;\n}\n\n/* ─── Modal-style: session expired while mid-action ─────────────────────── */\n\nfunction ExpiredModalView() {\n const [pw, setPw] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n\n return (\n <div className=\"min-h-screen relative\" style={{ background: \"var(--color-bg-default)\" }}>\n {/* Dimmed pseudo-app behind modal */}\n <FakeAppBackdrop />\n\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\" style={{ background: \"color-mix(in srgb, var(--gray-950) 70%, transparent)\", backdropFilter: \"blur(2px)\" }}>\n <div className=\"w-full max-w-md rounded-2xl p-6 shadow-2xl\" style={{ background: \"var(--color-bg-default)\", border: \"1px solid var(--color-border-default)\" }}>\n <div className=\"flex items-start gap-3 mb-5\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Clock size={18} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1\">\n <h2 className=\"text-base font-semibold mb-1\">Your session expired</h2>\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\", lineHeight: 1.55 }}>\n For your security, we signed you out after a period of inactivity. Sign back in to continue where you left off.\n </p>\n </div>\n </div>\n\n <form onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => window.location.href = \"/welcome\", 1000); }} className=\"space-y-3\">\n <Field label=\"Password\" htmlFor=\"pw\">\n <PasswordInput id=\"pw\" value={pw} onChange={setPw} autoComplete=\"current-password\" />\n </Field>\n <div className=\"flex gap-2 pt-1\">\n <Link href=\"/login\" className=\"flex-1\">\n <Button type=\"button\" variant=\"outline\" className=\"w-full\">Sign in fresh</Button>\n </Link>\n <Button type=\"submit\" variant=\"brand\" disabled={pw.length < 4 || submitting} className=\"flex-1\">\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Restoring…</> : \"Continue\"}\n </Button>\n </div>\n </form>\n\n <p className=\"text-[11px] mt-4 pt-4 border-t text-center\" style={{ borderColor: \"var(--color-border-muted)\", color: \"var(--color-text-muted)\" }}>\n Signed in as <strong style={{ color: \"var(--color-text-primary)\" }}>sarah.park@kcl.ac.uk</strong>\n </p>\n </div>\n </div>\n </div>\n );\n}\n\nfunction FakeAppBackdrop() {\n return (\n <div className=\"absolute inset-0 overflow-hidden\" aria-hidden>\n <div className=\"h-14 border-b px-6 flex items-center gap-3\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-6 h-6 rounded\" style={{ background: \"var(--color-bg-ui)\" }} />\n <div className=\"w-24 h-3 rounded\" style={{ background: \"var(--color-bg-ui)\" }} />\n </div>\n <div className=\"grid grid-cols-12 gap-6 p-8\">\n <div className=\"col-span-3 space-y-3\">\n {[1,2,3,4].map(i => <div key={i} className=\"h-8 rounded\" style={{ background: \"var(--color-bg-subtle)\" }} />)}\n </div>\n <div className=\"col-span-9 space-y-4\">\n <div className=\"h-8 w-1/3 rounded\" style={{ background: \"var(--color-bg-subtle)\" }} />\n <div className=\"grid grid-cols-3 gap-4\">\n {[1,2,3].map(i => <div key={i} className=\"h-32 rounded-xl\" style={{ background: \"var(--color-bg-subtle)\" }} />)}\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n {[1,2].map(i => <div key={i} className=\"h-48 rounded-xl\" style={{ background: \"var(--color-bg-subtle)\" }} />)}\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n/* ─── Full-page: returned after long absence ────────────────────────────── */\n\nfunction ExpiredFullView() {\n return (\n <AuthShell kicker=\"Welcome back\" title=\"Your session has expired\" subtitle=\"It's been a while since you were last here. Sign in to continue your work.\">\n <div className=\"space-y-4\">\n <InlineAlert variant=\"info\">\n We sign users out after 30 days of inactivity to keep accounts secure.\n </InlineAlert>\n <Link href=\"/login\"><Button variant=\"brand\" size=\"lg\" className=\"w-full\">Sign back in <ArrowRight size={14} /></Button></Link>\n </div>\n </AuthShell>\n );\n}\n\n/* ─── Logged out (manual sign-out confirmation) ─────────────────────────── */\n\nfunction LoggedOutView() {\n const [seconds, setSeconds] = useState(5);\n useEffect(() => {\n if (seconds > 0) {\n const t = setTimeout(() => setSeconds(s => s - 1), 1000);\n return () => clearTimeout(t);\n } else {\n window.location.href = \"/marketing\";\n }\n }, [seconds]);\n\n return (\n <AuthShell kicker=\"See you soon\" title=\"You're signed out\" subtitle=\"Your session ended. Redirecting you to the homepage…\">\n <div className=\"flex flex-col items-center text-center py-4 space-y-5\">\n <div className=\"w-16 h-16 rounded-full flex items-center justify-center\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <LogOut size={28} strokeWidth={1.6} />\n </div>\n <p className=\"text-sm flex items-center gap-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Spinner className=\"w-4 h-4\" /> Redirecting in {seconds}s…\n </p>\n <div className=\"flex flex-col gap-2 w-full pt-2\">\n <Link href=\"/login\"><Button variant=\"brand\" size=\"lg\" className=\"w-full\">Sign in again</Button></Link>\n <Link href=\"/marketing\"><Button variant=\"outline\" size=\"lg\" className=\"w-full\">Go to homepage</Button></Link>\n </div>\n </div>\n </AuthShell>\n );\n}\n\n/* ─── Concurrent session warning ────────────────────────────────────────── */\n\nfunction ConcurrentSessionView() {\n return (\n <AuthShell kicker=\"Active session detected\" title=\"You're signed in elsewhere\" subtitle=\"Your account is currently active on another device. For security, only one active session is allowed at a time.\">\n <div className=\"space-y-4\">\n <div className=\"rounded-2xl border\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"p-4 flex items-start gap-3\" style={{ borderBottom: \"1px solid var(--color-border-muted)\" }}>\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <MonitorSmartphone size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium flex items-center gap-2\">\n Chrome on MacBook Pro\n <span className=\"text-[10px] font-medium px-1.5 py-0.5 rounded-md\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>Current</span>\n </div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>London, UK · Active 2 minutes ago</div>\n </div>\n </div>\n <div className=\"p-4 flex items-start gap-3\">\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <MonitorSmartphone size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">Safari on iPhone</div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>London, UK · Active just now</div>\n </div>\n </div>\n </div>\n\n <div className=\"flex flex-col gap-2 pt-1\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">Sign out other devices & continue here <ArrowRight size={14} /></Button>\n <Link href=\"/login\"><Button variant=\"outline\" size=\"lg\" className=\"w-full\">Cancel and sign out</Button></Link>\n </div>\n\n <p className=\"text-[11px] text-center pt-2\" style={{ color: \"var(--color-text-muted)\" }}>\n Don't recognize the other device?{\" \"}\n <Link href=\"/locked?state=suspicious\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-status-error-text)\" }}>\n Secure your account\n </Link>.\n </p>\n </div>\n </AuthShell>\n );\n}\n"
62
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/(auth)/signup/page.tsx",
4
+ "sha256": "e0df80aedf075567da0c2a653c801c312b4641b37383c314eee00d637709883d",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "next/navigation",
9
+ "lucide-react",
10
+ "@/components/ui/button",
11
+ "@/components/ui/input",
12
+ "@/components/auth/auth-shell",
13
+ "@/components/auth/auth-primitives"
14
+ ],
15
+ "tokens": [
16
+ "--color-bg-brand-muted",
17
+ "--color-border-brand",
18
+ "--color-border-default",
19
+ "--color-text-brand",
20
+ "--color-text-muted",
21
+ "--color-text-primary",
22
+ "--color-text-secondary",
23
+ "--teal-500"
24
+ ],
25
+ "fragments": [
26
+ {
27
+ "id": "SignupPage",
28
+ "line": 23,
29
+ "code": "export default function SignupPage() {\n const search = useSearchParams();\n const forced = search.get(\"state\") as State | null;\n const roleFromUrl = (search.get(\"role\") as Role | null);\n\n const [role, setRole] = useState<Role | null>(roleFromUrl ?? null);\n const [fullName, setFullName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [terms, setTerms] = useState(false);\n const [marketingOptIn, setMarketingOptIn] = useState(false);\n const [socialLoading, setSocialLoading] = useState<null | \"google\" | \"apple\" | \"github\">(null);\n const [submitting, setSubmitting] = useState(false);\n const [errorKind, setErrorKind] = useState<null | \"invite-only\" | \"email-taken\">(null);\n\n const state: State = forced ?? (submitting ? \"loading\" : (errorKind ?? \"idle\"));\n const { score } = passwordStrength(password);\n const canSubmit = !!role && fullName.length >= 2 && /.+@.+\\..+/.test(email) && score >= 2 && terms;\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setSubmitting(true);\n setErrorKind(null);\n setTimeout(() => {\n setSubmitting(false);\n if (email.includes(\"invite-only\")) setErrorKind(\"invite-only\");\n else if (email.includes(\"taken\")) setErrorKind(\"email-taken\");\n else {\n // Persist role for onboarding\n try { localStorage.setItem(\"mentix.role\", role!); } catch {}\n window.location.href = \"/verify-email?email=\" + encodeURIComponent(email);\n }\n }, 1000);\n };\n\n // ─── Invite-only state ──\n if (state === \"invite-only\") {\n return (\n <AuthShell kicker=\"Closed beta\" title=\"Mentix is invite-only right now\" subtitle=\"We're rolling out access in waves. Join the waitlist and we'll reach out as soon as your specialty is supported.\">\n <div className=\"space-y-4\">\n <InlineAlert variant=\"info\" title=\"You're not on the list — yet\">\n Mentix is currently in closed beta with a focus on ESD trainees across the UK, EU, and APAC.\n </InlineAlert>\n <form className=\"flex gap-2\">\n <Input className=\"h-10\" placeholder=\"you@hospital.com\" defaultValue={email} type=\"email\" />\n <Button variant=\"brand\">Request access</Button>\n </form>\n <p className=\"text-[12px] text-center\" style={{ color: \"var(--color-text-muted)\" }}>\n Already have an invite code?{\" \"}\n <Link href=\"/invite\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>Enter it here</Link>\n </p>\n </div>\n </AuthShell>\n );\n }\n\n return (\n <AuthShell\n title=\"Create your account\"\n subtitle=\"14-day free trial. No card required.\"\n footer={\n <>\n Already have an account?{\" \"}\n <Link href=\"/login\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n Sign in\n </Link>\n </>\n }\n >\n {state === \"email-taken\" && (\n <div className=\"mb-5\">\n <InlineAlert\n variant=\"warning\"\n title=\"That email is already registered\"\n action={<Link href=\"/login\" className=\"text-[12px] font-medium hover:underline underline-offset-4\">Sign in →</Link>}\n >\n Try signing in instead, or use a different email to create a new account.\n </InlineAlert>\n </div>\n )}\n\n {/* Role selector */}\n <div className=\"mb-5\">\n <p className=\"text-[13px] font-medium mb-2.5\" style={{ color: \"var(--color-text-primary)\" }}>I'm joining as a…</p>\n <div className=\"grid grid-cols-3 gap-2\">\n {ROLES.map((r) => {\n const Icon = r.icon;\n const active = role === r.id;\n return (\n <button\n key={r.id}\n type=\"button\"\n onClick={() => setRole(r.id)}\n aria-pressed={active}\n className=\"p-3 rounded-xl border transition-all text-left focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n style={{\n borderColor: active ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: active ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n }}\n >\n <Icon\n size={16}\n strokeWidth={1.8}\n className=\"mb-2\"\n style={{ color: active ? \"var(--color-text-brand)\" : \"var(--color-text-secondary)\" }}\n />\n <div className=\"text-[13px] font-semibold\" style={{ color: active ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\" }}>\n {r.label}\n </div>\n <div className=\"text-[11px] mt-0.5\" style={{ color: \"var(--color-text-muted)\" }}>{r.hint}</div>\n </button>\n );\n })}\n </div>\n </div>\n\n <SocialButtons\n onClick={(p) => { setSocialLoading(p); setTimeout(() => setSocialLoading(null), 1500); }}\n loading={socialLoading}\n />\n\n <OrDivider />\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\" noValidate>\n <Field label=\"Full name\" htmlFor=\"name\">\n <Input id=\"name\" value={fullName} onChange={(e) => setFullName(e.target.value)} placeholder=\"Dr. Amyn Haji\" autoComplete=\"name\" className=\"h-10\" required />\n </Field>\n\n <Field label=\"Work email\" htmlFor=\"email\" hint=\"Use your hospital or institutional email.\">\n <Input id=\"email\" type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} placeholder=\"you@hospital.com\" autoComplete=\"email\" className=\"h-10\" required />\n </Field>\n\n <Field label=\"Password\" htmlFor=\"password\">\n <PasswordInput id=\"password\" value={password} onChange={setPassword} placeholder=\"Choose a strong password\" autoComplete=\"new-password\" />\n {password && <PasswordStrength password={password} />}\n </Field>\n\n <div className=\"space-y-2 mt-1\">\n <Checkbox id=\"terms\" checked={terms} onChange={setTerms}>\n I agree to the <Link href=\"#\" className=\"underline underline-offset-4 hover:opacity-70\" style={{ color: \"var(--color-text-primary)\" }}>Terms of Service</Link>\n {\" \"}and{\" \"}\n <Link href=\"#\" className=\"underline underline-offset-4 hover:opacity-70\" style={{ color: \"var(--color-text-primary)\" }}>Privacy Policy</Link>.\n </Checkbox>\n <Checkbox id=\"marketing\" checked={marketingOptIn} onChange={setMarketingOptIn}>\n Send me product updates and clinical training resources.\n </Checkbox>\n </div>\n\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" disabled={!canSubmit || submitting} className=\"mt-2\">\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Creating account…</> : <>Create account <ArrowRight size={14} /></>}\n </Button>\n\n <p className=\"text-[11px] text-center\" style={{ color: \"var(--color-text-muted)\" }}>\n Protected by industry-standard encryption. SOC 2 Type II · HIPAA · UK GDPR.\n </p>\n </form>\n </AuthShell>\n );\n}"
30
+ }
31
+ ],
32
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowRight, GraduationCap, Stethoscope, Building2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport {\n Field, PasswordInput, PasswordStrength, OrDivider, InlineAlert, Checkbox, SocialButtons, Spinner, passwordStrength,\n} from \"@/components/auth/auth-primitives\";\n\ntype State = \"idle\" | \"loading\" | \"invite-only\" | \"email-taken\";\ntype Role = \"trainee\" | \"mentor\" | \"sponsor\";\n\nconst ROLES: { id: Role; icon: typeof GraduationCap; label: string; hint: string }[] = [\n { id: \"trainee\", icon: GraduationCap, label: \"Trainee\", hint: \"Learn from experts\" },\n { id: \"mentor\", icon: Stethoscope, label: \"Mentor\", hint: \"Share your expertise\" },\n { id: \"sponsor\", icon: Building2, label: \"Sponsor\", hint: \"Fund training\" },\n];\n\nexport default function SignupPage() {\n const search = useSearchParams();\n const forced = search.get(\"state\") as State | null;\n const roleFromUrl = (search.get(\"role\") as Role | null);\n\n const [role, setRole] = useState<Role | null>(roleFromUrl ?? null);\n const [fullName, setFullName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [terms, setTerms] = useState(false);\n const [marketingOptIn, setMarketingOptIn] = useState(false);\n const [socialLoading, setSocialLoading] = useState<null | \"google\" | \"apple\" | \"github\">(null);\n const [submitting, setSubmitting] = useState(false);\n const [errorKind, setErrorKind] = useState<null | \"invite-only\" | \"email-taken\">(null);\n\n const state: State = forced ?? (submitting ? \"loading\" : (errorKind ?? \"idle\"));\n const { score } = passwordStrength(password);\n const canSubmit = !!role && fullName.length >= 2 && /.+@.+\\..+/.test(email) && score >= 2 && terms;\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setSubmitting(true);\n setErrorKind(null);\n setTimeout(() => {\n setSubmitting(false);\n if (email.includes(\"invite-only\")) setErrorKind(\"invite-only\");\n else if (email.includes(\"taken\")) setErrorKind(\"email-taken\");\n else {\n // Persist role for onboarding\n try { localStorage.setItem(\"mentix.role\", role!); } catch {}\n window.location.href = \"/verify-email?email=\" + encodeURIComponent(email);\n }\n }, 1000);\n };\n\n // ─── Invite-only state ──\n if (state === \"invite-only\") {\n return (\n <AuthShell kicker=\"Closed beta\" title=\"Mentix is invite-only right now\" subtitle=\"We're rolling out access in waves. Join the waitlist and we'll reach out as soon as your specialty is supported.\">\n <div className=\"space-y-4\">\n <InlineAlert variant=\"info\" title=\"You're not on the list — yet\">\n Mentix is currently in closed beta with a focus on ESD trainees across the UK, EU, and APAC.\n </InlineAlert>\n <form className=\"flex gap-2\">\n <Input className=\"h-10\" placeholder=\"you@hospital.com\" defaultValue={email} type=\"email\" />\n <Button variant=\"brand\">Request access</Button>\n </form>\n <p className=\"text-[12px] text-center\" style={{ color: \"var(--color-text-muted)\" }}>\n Already have an invite code?{\" \"}\n <Link href=\"/invite\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>Enter it here</Link>\n </p>\n </div>\n </AuthShell>\n );\n }\n\n return (\n <AuthShell\n title=\"Create your account\"\n subtitle=\"14-day free trial. No card required.\"\n footer={\n <>\n Already have an account?{\" \"}\n <Link href=\"/login\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n Sign in\n </Link>\n </>\n }\n >\n {state === \"email-taken\" && (\n <div className=\"mb-5\">\n <InlineAlert\n variant=\"warning\"\n title=\"That email is already registered\"\n action={<Link href=\"/login\" className=\"text-[12px] font-medium hover:underline underline-offset-4\">Sign in →</Link>}\n >\n Try signing in instead, or use a different email to create a new account.\n </InlineAlert>\n </div>\n )}\n\n {/* Role selector */}\n <div className=\"mb-5\">\n <p className=\"text-[13px] font-medium mb-2.5\" style={{ color: \"var(--color-text-primary)\" }}>I'm joining as a…</p>\n <div className=\"grid grid-cols-3 gap-2\">\n {ROLES.map((r) => {\n const Icon = r.icon;\n const active = role === r.id;\n return (\n <button\n key={r.id}\n type=\"button\"\n onClick={() => setRole(r.id)}\n aria-pressed={active}\n className=\"p-3 rounded-xl border transition-all text-left focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n style={{\n borderColor: active ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: active ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n }}\n >\n <Icon\n size={16}\n strokeWidth={1.8}\n className=\"mb-2\"\n style={{ color: active ? \"var(--color-text-brand)\" : \"var(--color-text-secondary)\" }}\n />\n <div className=\"text-[13px] font-semibold\" style={{ color: active ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\" }}>\n {r.label}\n </div>\n <div className=\"text-[11px] mt-0.5\" style={{ color: \"var(--color-text-muted)\" }}>{r.hint}</div>\n </button>\n );\n })}\n </div>\n </div>\n\n <SocialButtons\n onClick={(p) => { setSocialLoading(p); setTimeout(() => setSocialLoading(null), 1500); }}\n loading={socialLoading}\n />\n\n <OrDivider />\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\" noValidate>\n <Field label=\"Full name\" htmlFor=\"name\">\n <Input id=\"name\" value={fullName} onChange={(e) => setFullName(e.target.value)} placeholder=\"Dr. Amyn Haji\" autoComplete=\"name\" className=\"h-10\" required />\n </Field>\n\n <Field label=\"Work email\" htmlFor=\"email\" hint=\"Use your hospital or institutional email.\">\n <Input id=\"email\" type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} placeholder=\"you@hospital.com\" autoComplete=\"email\" className=\"h-10\" required />\n </Field>\n\n <Field label=\"Password\" htmlFor=\"password\">\n <PasswordInput id=\"password\" value={password} onChange={setPassword} placeholder=\"Choose a strong password\" autoComplete=\"new-password\" />\n {password && <PasswordStrength password={password} />}\n </Field>\n\n <div className=\"space-y-2 mt-1\">\n <Checkbox id=\"terms\" checked={terms} onChange={setTerms}>\n I agree to the <Link href=\"#\" className=\"underline underline-offset-4 hover:opacity-70\" style={{ color: \"var(--color-text-primary)\" }}>Terms of Service</Link>\n {\" \"}and{\" \"}\n <Link href=\"#\" className=\"underline underline-offset-4 hover:opacity-70\" style={{ color: \"var(--color-text-primary)\" }}>Privacy Policy</Link>.\n </Checkbox>\n <Checkbox id=\"marketing\" checked={marketingOptIn} onChange={setMarketingOptIn}>\n Send me product updates and clinical training resources.\n </Checkbox>\n </div>\n\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" disabled={!canSubmit || submitting} className=\"mt-2\">\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Creating account…</> : <>Create account <ArrowRight size={14} /></>}\n </Button>\n\n <p className=\"text-[11px] text-center\" style={{ color: \"var(--color-text-muted)\" }}>\n Protected by industry-standard encryption. SOC 2 Type II · HIPAA · UK GDPR.\n </p>\n </form>\n </AuthShell>\n );\n}\n"
33
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/(auth)/verify-email/page.tsx",
4
+ "sha256": "09fa69e0c94247aad896c292f713f5c88f3aba7df2a43e1bf158b01b0bfaca8b",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "next/navigation",
9
+ "lucide-react",
10
+ "@/components/ui/button",
11
+ "@/components/ui/input",
12
+ "@/components/auth/auth-shell",
13
+ "@/components/auth/auth-primitives"
14
+ ],
15
+ "tokens": [
16
+ "--color-bg-brand-muted",
17
+ "--color-border-default",
18
+ "--color-border-muted",
19
+ "--color-status-success-bg",
20
+ "--color-status-success-text",
21
+ "--color-text-brand",
22
+ "--color-text-muted",
23
+ "--color-text-secondary",
24
+ "--teal-500"
25
+ ],
26
+ "fragments": [
27
+ {
28
+ "id": "VerifyEmailPage",
29
+ "line": 13,
30
+ "code": "export default function VerifyEmailPage() {\n const search = useSearchParams();\n const forced = search.get(\"state\") as State | null;\n const initialEmail = search.get(\"email\") ?? \"you@hospital.com\";\n\n const [email, setEmail] = useState(initialEmail);\n const [editing, setEditing] = useState(false);\n const [draftEmail, setDraftEmail] = useState(initialEmail);\n const [resendCooldown, setResendCooldown] = useState(0);\n const [resending, setResending] = useState(false);\n\n const state: State = forced ?? \"waiting\";\n\n useEffect(() => {\n if (resendCooldown > 0) {\n const t = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000);\n return () => clearTimeout(t);\n }\n }, [resendCooldown]);\n\n const resend = () => {\n if (resendCooldown > 0) return;\n setResending(true);\n setTimeout(() => { setResending(false); setResendCooldown(45); }, 800);\n };\n\n // ─── Success ──\n if (state === \"success\") {\n return (\n <AuthShell kicker=\"Email verified\" title=\"You're all set\" subtitle=\"Your email is verified. Redirecting to your workspace…\">\n <div className=\"flex flex-col items-center text-center py-4\">\n <div className=\"w-16 h-16 rounded-full flex items-center justify-center mb-5\"\n style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}>\n <CheckCircle2 size={32} strokeWidth={1.6} />\n </div>\n <p className=\"text-sm flex items-center gap-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Spinner className=\"w-4 h-4\" /> Taking you to Mentix…\n </p>\n <Link href=\"/welcome\" className=\"text-[12px] mt-4 hover:underline underline-offset-4\" style={{ color: \"var(--color-text-muted)\" }}>\n Continue manually\n </Link>\n </div>\n </AuthShell>\n );\n }\n\n // ─── Expired token ──\n if (state === \"expired\") {\n return (\n <AuthShell kicker=\"Link expired\" title=\"This verification link has expired\" subtitle=\"Verification links are valid for 24 hours. We'll send you a fresh one.\">\n <div className=\"space-y-4\">\n <InlineAlert variant=\"warning\">\n The link you used has expired. Click below and we'll send a new verification email.\n </InlineAlert>\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\" onClick={resend} disabled={resending}>\n {resending ? <><Spinner className=\"w-4 h-4 mr-1\" /> Sending…</> : <>Send new verification link <ArrowRight size={14} /></>}\n </Button>\n </div>\n </AuthShell>\n );\n }\n\n // ─── Waiting ──\n return (\n <AuthShell kicker=\"Almost there\" title=\"Check your inbox\" subtitle=\"We sent a verification link to confirm your email. Click the link in the email to continue.\">\n <div className=\"space-y-5\">\n {/* Email card */}\n <div className=\"rounded-2xl border p-5\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"flex items-center gap-4\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Mail size={18} strokeWidth={1.8} />\n </div>\n {editing ? (\n <form className=\"flex-1 flex gap-2\" onSubmit={(e) => { e.preventDefault(); setEmail(draftEmail); setEditing(false); }}>\n <Input value={draftEmail} onChange={(e) => setDraftEmail(e.target.value)} type=\"email\" className=\"h-9\" autoFocus />\n <Button type=\"submit\" size=\"sm\" variant=\"brand\">Save</Button>\n </form>\n ) : (\n <>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium truncate\">{email}</div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>Verification email sent</div>\n </div>\n <button onClick={() => { setEditing(true); setDraftEmail(email); }} className=\"text-[12px] inline-flex items-center gap-1 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Edit2 size={12} strokeWidth={1.8} /> Edit\n </button>\n </>\n )}\n </div>\n </div>\n\n {/* Status */}\n <div className=\"flex items-center gap-3 text-[12px] py-1\" style={{ color: \"var(--color-text-secondary)\" }}>\n <div className=\"w-1.5 h-1.5 rounded-full animate-pulse\" style={{ background: \"var(--teal-500)\" }} />\n Waiting for verification…\n </div>\n\n {/* Resend */}\n <div className=\"text-[12px] space-y-1.5\" style={{ color: \"var(--color-text-secondary)\" }}>\n <p>\n Didn't get the email? Check your spam folder, or{\" \"}\n <button onClick={resend} disabled={resendCooldown > 0 || resending} className=\"font-medium hover:underline underline-offset-4 disabled:opacity-50 disabled:no-underline\" style={{ color: \"var(--color-text-brand)\" }}>\n {resending ? \"sending…\" : resendCooldown > 0 ? `resend in ${resendCooldown}s` : \"resend it\"}\n </button>.\n </p>\n <p>\n Need help?{\" \"}\n <Link href=\"#\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>Contact support</Link>\n </p>\n </div>\n\n <div className=\"text-center pt-3 border-t\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <Link href=\"/login\" className=\"text-[12px] hover:underline underline-offset-4\" style={{ color: \"var(--color-text-muted)\" }}>\n Already verified? Sign in →\n </Link>\n </div>\n </div>\n </AuthShell>\n );\n}"
31
+ }
32
+ ],
33
+ "content": "\"use client\";\nimport Link from \"next/link\";\nimport { useState, useMemo, useEffect } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowRight, CheckCircle2, Mail, Edit2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport { Field, InlineAlert, Spinner } from \"@/components/auth/auth-primitives\";\n\ntype State = \"waiting\" | \"success\" | \"expired\";\n\nexport default function VerifyEmailPage() {\n const search = useSearchParams();\n const forced = search.get(\"state\") as State | null;\n const initialEmail = search.get(\"email\") ?? \"you@hospital.com\";\n\n const [email, setEmail] = useState(initialEmail);\n const [editing, setEditing] = useState(false);\n const [draftEmail, setDraftEmail] = useState(initialEmail);\n const [resendCooldown, setResendCooldown] = useState(0);\n const [resending, setResending] = useState(false);\n\n const state: State = forced ?? \"waiting\";\n\n useEffect(() => {\n if (resendCooldown > 0) {\n const t = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000);\n return () => clearTimeout(t);\n }\n }, [resendCooldown]);\n\n const resend = () => {\n if (resendCooldown > 0) return;\n setResending(true);\n setTimeout(() => { setResending(false); setResendCooldown(45); }, 800);\n };\n\n // ─── Success ──\n if (state === \"success\") {\n return (\n <AuthShell kicker=\"Email verified\" title=\"You're all set\" subtitle=\"Your email is verified. Redirecting to your workspace…\">\n <div className=\"flex flex-col items-center text-center py-4\">\n <div className=\"w-16 h-16 rounded-full flex items-center justify-center mb-5\"\n style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}>\n <CheckCircle2 size={32} strokeWidth={1.6} />\n </div>\n <p className=\"text-sm flex items-center gap-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Spinner className=\"w-4 h-4\" /> Taking you to Mentix…\n </p>\n <Link href=\"/welcome\" className=\"text-[12px] mt-4 hover:underline underline-offset-4\" style={{ color: \"var(--color-text-muted)\" }}>\n Continue manually\n </Link>\n </div>\n </AuthShell>\n );\n }\n\n // ─── Expired token ──\n if (state === \"expired\") {\n return (\n <AuthShell kicker=\"Link expired\" title=\"This verification link has expired\" subtitle=\"Verification links are valid for 24 hours. We'll send you a fresh one.\">\n <div className=\"space-y-4\">\n <InlineAlert variant=\"warning\">\n The link you used has expired. Click below and we'll send a new verification email.\n </InlineAlert>\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\" onClick={resend} disabled={resending}>\n {resending ? <><Spinner className=\"w-4 h-4 mr-1\" /> Sending…</> : <>Send new verification link <ArrowRight size={14} /></>}\n </Button>\n </div>\n </AuthShell>\n );\n }\n\n // ─── Waiting ──\n return (\n <AuthShell kicker=\"Almost there\" title=\"Check your inbox\" subtitle=\"We sent a verification link to confirm your email. Click the link in the email to continue.\">\n <div className=\"space-y-5\">\n {/* Email card */}\n <div className=\"rounded-2xl border p-5\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"flex items-center gap-4\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Mail size={18} strokeWidth={1.8} />\n </div>\n {editing ? (\n <form className=\"flex-1 flex gap-2\" onSubmit={(e) => { e.preventDefault(); setEmail(draftEmail); setEditing(false); }}>\n <Input value={draftEmail} onChange={(e) => setDraftEmail(e.target.value)} type=\"email\" className=\"h-9\" autoFocus />\n <Button type=\"submit\" size=\"sm\" variant=\"brand\">Save</Button>\n </form>\n ) : (\n <>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium truncate\">{email}</div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>Verification email sent</div>\n </div>\n <button onClick={() => { setEditing(true); setDraftEmail(email); }} className=\"text-[12px] inline-flex items-center gap-1 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Edit2 size={12} strokeWidth={1.8} /> Edit\n </button>\n </>\n )}\n </div>\n </div>\n\n {/* Status */}\n <div className=\"flex items-center gap-3 text-[12px] py-1\" style={{ color: \"var(--color-text-secondary)\" }}>\n <div className=\"w-1.5 h-1.5 rounded-full animate-pulse\" style={{ background: \"var(--teal-500)\" }} />\n Waiting for verification…\n </div>\n\n {/* Resend */}\n <div className=\"text-[12px] space-y-1.5\" style={{ color: \"var(--color-text-secondary)\" }}>\n <p>\n Didn't get the email? Check your spam folder, or{\" \"}\n <button onClick={resend} disabled={resendCooldown > 0 || resending} className=\"font-medium hover:underline underline-offset-4 disabled:opacity-50 disabled:no-underline\" style={{ color: \"var(--color-text-brand)\" }}>\n {resending ? \"sending…\" : resendCooldown > 0 ? `resend in ${resendCooldown}s` : \"resend it\"}\n </button>.\n </p>\n <p>\n Need help?{\" \"}\n <Link href=\"#\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>Contact support</Link>\n </p>\n </div>\n\n <div className=\"text-center pt-3 border-t\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <Link href=\"/login\" className=\"text-[12px] hover:underline underline-offset-4\" style={{ color: \"var(--color-text-muted)\" }}>\n Already verified? Sign in →\n </Link>\n </div>\n </div>\n </AuthShell>\n );\n}\n"
34
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/(auth)/welcome/page.tsx",
4
+ "sha256": "375f16cdbf3e7bcea946fe15b2fb7d35fe87e7a882d5f567590f4351e9a64be1",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "next/navigation",
9
+ "lucide-react",
10
+ "@/components/ui/button",
11
+ "@/components/auth/auth-shell",
12
+ "@/components/auth/auth-primitives"
13
+ ],
14
+ "tokens": [
15
+ "--color-bg-default",
16
+ "--color-status-success-bg",
17
+ "--color-status-success-text",
18
+ "--color-text-muted",
19
+ "--color-text-secondary",
20
+ "--teal-400",
21
+ "--teal-500"
22
+ ],
23
+ "fragments": [
24
+ {
25
+ "id": "WelcomePage",
26
+ "line": 13,
27
+ "code": "export default function WelcomePage() {\n const search = useSearchParams();\n const stateParam = (search.get(\"state\") as State | null) ?? \"celebrate\";\n\n if (stateParam === \"redirect\") return <RedirectView />;\n return <CelebrateView />;\n}"
28
+ },
29
+ {
30
+ "id": "CelebrateView",
31
+ "line": 23,
32
+ "code": "function CelebrateView() {\n return (\n <AuthShell title={<>You're in.</>} subtitle=\"Your account is ready. Set up your profile now or come back to it later.\">\n <div className=\"space-y-6\">\n {/* Success mark */}\n <div className=\"flex justify-center pt-2 pb-1\">\n <div className=\"relative\">\n <div\n className=\"w-20 h-20 rounded-full flex items-center justify-center\"\n style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}\n >\n <CheckCircle2 size={40} strokeWidth={1.5} />\n </div>\n <Sparkles size={16} className=\"absolute -top-1 -right-1\" style={{ color: \"var(--teal-500)\" }} />\n <Sparkles size={12} className=\"absolute -bottom-1 -left-2\" style={{ color: \"var(--teal-400)\" }} />\n </div>\n </div>\n\n {/* CTAs */}\n <div className=\"flex flex-col gap-2 pt-2\">\n <Link href=\"/onboarding\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">\n Set up your profile <ArrowRight size={14} />\n </Button>\n </Link>\n <Link href=\"/marketing\">\n <Button variant=\"outline\" size=\"lg\" className=\"w-full\">\n Skip for now\n </Button>\n </Link>\n </div>\n\n <p className=\"text-[12px] text-center pt-1\" style={{ color: \"var(--color-text-muted)\" }}>\n You can finish onboarding anytime from your account settings.\n </p>\n </div>\n </AuthShell>\n );\n}"
33
+ },
34
+ {
35
+ "id": "RedirectView",
36
+ "line": 65,
37
+ "code": "function RedirectView() {\n const [secs, setSecs] = useState(3);\n useEffect(() => {\n if (secs > 0) {\n const t = setTimeout(() => setSecs((s) => s - 1), 1000);\n return () => clearTimeout(t);\n } else {\n window.location.href = \"/marketing\";\n }\n }, [secs]);\n\n return (\n <div className=\"min-h-screen flex items-center justify-center\" style={{ background: \"var(--color-bg-default)\" }}>\n <div className=\"text-center\">\n <div className=\"flex items-center justify-center mb-6\">\n <img src=\"/mentix-logo.png\" alt=\"Mentix\" style={{ height: 48, width: 48, objectFit: \"contain\" }} />\n </div>\n <Spinner className=\"w-6 h-6 mx-auto mb-4\" />\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>Taking you in…</p>\n <p className=\"text-[11px] mt-1\" style={{ color: \"var(--color-text-muted)\" }}>Redirecting in {secs}s</p>\n </div>\n </div>\n );\n}"
38
+ }
39
+ ],
40
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useState, useEffect } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowRight, CheckCircle2, Sparkles } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport { Spinner } from \"@/components/auth/auth-primitives\";\n\ntype State = \"celebrate\" | \"redirect\";\n\nexport default function WelcomePage() {\n const search = useSearchParams();\n const stateParam = (search.get(\"state\") as State | null) ?? \"celebrate\";\n\n if (stateParam === \"redirect\") return <RedirectView />;\n return <CelebrateView />;\n}\n\n/* ─── Celebrate (post-signup) ────────────────────────────────────────────── */\n\nfunction CelebrateView() {\n return (\n <AuthShell title={<>You're in.</>} subtitle=\"Your account is ready. Set up your profile now or come back to it later.\">\n <div className=\"space-y-6\">\n {/* Success mark */}\n <div className=\"flex justify-center pt-2 pb-1\">\n <div className=\"relative\">\n <div\n className=\"w-20 h-20 rounded-full flex items-center justify-center\"\n style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}\n >\n <CheckCircle2 size={40} strokeWidth={1.5} />\n </div>\n <Sparkles size={16} className=\"absolute -top-1 -right-1\" style={{ color: \"var(--teal-500)\" }} />\n <Sparkles size={12} className=\"absolute -bottom-1 -left-2\" style={{ color: \"var(--teal-400)\" }} />\n </div>\n </div>\n\n {/* CTAs */}\n <div className=\"flex flex-col gap-2 pt-2\">\n <Link href=\"/onboarding\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\">\n Set up your profile <ArrowRight size={14} />\n </Button>\n </Link>\n <Link href=\"/marketing\">\n <Button variant=\"outline\" size=\"lg\" className=\"w-full\">\n Skip for now\n </Button>\n </Link>\n </div>\n\n <p className=\"text-[12px] text-center pt-1\" style={{ color: \"var(--color-text-muted)\" }}>\n You can finish onboarding anytime from your account settings.\n </p>\n </div>\n </AuthShell>\n );\n}\n\n/* ─── Redirect transition ───────────────────────────────────────────────── */\n\nfunction RedirectView() {\n const [secs, setSecs] = useState(3);\n useEffect(() => {\n if (secs > 0) {\n const t = setTimeout(() => setSecs((s) => s - 1), 1000);\n return () => clearTimeout(t);\n } else {\n window.location.href = \"/marketing\";\n }\n }, [secs]);\n\n return (\n <div className=\"min-h-screen flex items-center justify-center\" style={{ background: \"var(--color-bg-default)\" }}>\n <div className=\"text-center\">\n <div className=\"flex items-center justify-center mb-6\">\n <img src=\"/mentix-logo.png\" alt=\"Mentix\" style={{ height: 48, width: 48, objectFit: \"contain\" }} />\n </div>\n <Spinner className=\"w-6 h-6 mx-auto mb-4\" />\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>Taking you in…</p>\n <p className=\"text-[11px] mt-1\" style={{ color: \"var(--color-text-muted)\" }}>Redirecting in {secs}s</p>\n </div>\n </div>\n );\n}\n"
41
+ }