@rebasepro/app 0.0.1-canary.4829d6e

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 (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/auth/api.d.ts +66 -0
  4. package/dist/auth/index.d.ts +13 -0
  5. package/dist/auth/types.d.ts +127 -0
  6. package/dist/auth/useRebaseAuthController.d.ts +13 -0
  7. package/dist/components/AIIcon.d.ts +17 -0
  8. package/dist/components/AuthSimulationMenu.d.ts +0 -0
  9. package/dist/components/ConfirmationDialog.d.ts +9 -0
  10. package/dist/components/Debug/UIReferenceView.d.ts +13 -0
  11. package/dist/components/Debug/UIStyleGuide.d.ts +2 -0
  12. package/dist/components/Debug/collection-views/CardViewDemo.d.ts +2 -0
  13. package/dist/components/Debug/collection-views/CollectionTableDemo.d.ts +2 -0
  14. package/dist/components/Debug/collection-views/KanbanBoardDemo.d.ts +2 -0
  15. package/dist/components/Debug/collection-views/index.d.ts +3 -0
  16. package/dist/components/Debug/collection-views/sample_data.d.ts +23 -0
  17. package/dist/components/Debug/crm-dashboard/CalendarWidget.d.ts +22 -0
  18. package/dist/components/Debug/crm-dashboard/CrmDashboardDemo.d.ts +2 -0
  19. package/dist/components/Debug/crm-dashboard/DashboardMetrics.d.ts +15 -0
  20. package/dist/components/Debug/crm-dashboard/PipelineOverview.d.ts +34 -0
  21. package/dist/components/Debug/crm-dashboard/RecentActivity.d.ts +19 -0
  22. package/dist/components/Debug/crm-dashboard/TaskFilters.d.ts +30 -0
  23. package/dist/components/Debug/crm-dashboard/TaskTable.d.ts +50 -0
  24. package/dist/components/Debug/crm-dashboard/TasksView.d.ts +41 -0
  25. package/dist/components/ErrorTooltip.d.ts +3 -0
  26. package/dist/components/ErrorView.d.ts +21 -0
  27. package/dist/components/LanguageToggle.d.ts +2 -0
  28. package/dist/components/LoginView/LoginView.d.ts +113 -0
  29. package/dist/components/LoginView/index.d.ts +2 -0
  30. package/dist/components/NotFoundPage.d.ts +2 -0
  31. package/dist/components/RebaseAuth.d.ts +10 -0
  32. package/dist/components/RebaseLogo.d.ts +7 -0
  33. package/dist/components/SchemaDriftBanner.d.ts +27 -0
  34. package/dist/components/UnsavedChangesDialog.d.ts +9 -0
  35. package/dist/components/UserDisplay.d.ts +7 -0
  36. package/dist/components/UserSelectPopover.d.ts +63 -0
  37. package/dist/components/UserSettingsView.d.ts +2 -0
  38. package/dist/components/common/index.d.ts +6 -0
  39. package/dist/components/common/table_height.d.ts +5 -0
  40. package/dist/components/common/types.d.ts +66 -0
  41. package/dist/components/common/useColumnsIds.d.ts +9 -0
  42. package/dist/components/common/useDataTableController.d.ts +45 -0
  43. package/dist/components/common/useDebouncedData.d.ts +9 -0
  44. package/dist/components/common/useScrollRestoration.d.ts +14 -0
  45. package/dist/components/index.d.ts +18 -0
  46. package/dist/contexts/AdminModeController.d.ts +4 -0
  47. package/dist/contexts/AnalyticsContext.d.ts +3 -0
  48. package/dist/contexts/AuthControllerContext.d.ts +3 -0
  49. package/dist/contexts/CollectionScopeContext.d.ts +50 -0
  50. package/dist/contexts/ComponentOverrideContext.d.ts +42 -0
  51. package/dist/contexts/CustomizationControllerContext.d.ts +3 -0
  52. package/dist/contexts/DataDriverContext.d.ts +3 -0
  53. package/dist/contexts/DataSourcesContext.d.ts +31 -0
  54. package/dist/contexts/DatabaseAdminContext.d.ts +3 -0
  55. package/dist/contexts/DialogsProvider.d.ts +4 -0
  56. package/dist/contexts/EffectiveRoleController.d.ts +4 -0
  57. package/dist/contexts/ModeController.d.ts +4 -0
  58. package/dist/contexts/RebaseClientInstanceContext.d.ts +6 -0
  59. package/dist/contexts/RebaseDataContext.d.ts +3 -0
  60. package/dist/contexts/SnackbarProvider.d.ts +2 -0
  61. package/dist/contexts/StorageSourceContext.d.ts +3 -0
  62. package/dist/contexts/StorageSourcesContext.d.ts +25 -0
  63. package/dist/contexts/UserConfigurationPersistenceContext.d.ts +3 -0
  64. package/dist/contexts/index.d.ts +17 -0
  65. package/dist/core/PluginLifecycleManager.d.ts +17 -0
  66. package/dist/core/PluginProviderStack.d.ts +21 -0
  67. package/dist/core/Rebase.d.ts +15 -0
  68. package/dist/core/RebaseProps.d.ts +291 -0
  69. package/dist/core/RebaseRouter.d.ts +4 -0
  70. package/dist/core/RebaseRoutes.d.ts +17 -0
  71. package/dist/core/index.d.ts +4 -0
  72. package/dist/hooks/ApiConfigContext.d.ts +24 -0
  73. package/dist/hooks/data/delete.d.ts +33 -0
  74. package/dist/hooks/data/save.d.ts +40 -0
  75. package/dist/hooks/data/useCollection.d.ts +62 -0
  76. package/dist/hooks/data/useData.d.ts +13 -0
  77. package/dist/hooks/data/useDataOrder.d.ts +12 -0
  78. package/dist/hooks/data/useFetch.d.ts +43 -0
  79. package/dist/hooks/data/useRelationSelector.d.ts +52 -0
  80. package/dist/hooks/index.d.ts +36 -0
  81. package/dist/hooks/useAdminModeController.d.ts +19 -0
  82. package/dist/hooks/useAnalyticsController.d.ts +5 -0
  83. package/dist/hooks/useAuthController.d.ts +11 -0
  84. package/dist/hooks/useAuthSubscription.d.ts +2 -0
  85. package/dist/hooks/useBackendStorageSource.d.ts +30 -0
  86. package/dist/hooks/useBridgeRegistration.d.ts +18 -0
  87. package/dist/hooks/useBrowserTitleAndIcon.d.ts +6 -0
  88. package/dist/hooks/useBuildAdminModeController.d.ts +6 -0
  89. package/dist/hooks/useBuildEffectiveRoleController.d.ts +8 -0
  90. package/dist/hooks/useBuildLocalConfigurationPersistence.d.ts +2 -0
  91. package/dist/hooks/useBuildModeController.d.ts +6 -0
  92. package/dist/hooks/useClipboard.d.ts +57 -0
  93. package/dist/hooks/useCollapsedGroups.d.ts +27 -0
  94. package/dist/hooks/useComponentOverride.d.ts +32 -0
  95. package/dist/hooks/useCustomizationController.d.ts +11 -0
  96. package/dist/hooks/useDialogsController.d.ts +11 -0
  97. package/dist/hooks/useEffectiveRoleController.d.ts +7 -0
  98. package/dist/hooks/useLargeLayout.d.ts +1 -0
  99. package/dist/hooks/useModeController.d.ts +19 -0
  100. package/dist/hooks/usePermissions.d.ts +12 -0
  101. package/dist/hooks/useRebaseClient.d.ts +5 -0
  102. package/dist/hooks/useRebaseContext.d.ts +11 -0
  103. package/dist/hooks/useRebaseRegistry.d.ts +34 -0
  104. package/dist/hooks/useResolvedComponent.d.ts +47 -0
  105. package/dist/hooks/useSlot.d.ts +18 -0
  106. package/dist/hooks/useSnackbarController.d.ts +20 -0
  107. package/dist/hooks/useStorageSource.d.ts +6 -0
  108. package/dist/hooks/useStudioBridge.d.ts +79 -0
  109. package/dist/hooks/useTranslation.d.ts +17 -0
  110. package/dist/hooks/useUnsavedChangesDialog.d.ts +12 -0
  111. package/dist/hooks/useUserConfigurationPersistence.d.ts +8 -0
  112. package/dist/i18n/RebaseI18nProvider.d.ts +33 -0
  113. package/dist/index.d.ts +16 -0
  114. package/dist/index.es.js +16843 -0
  115. package/dist/index.es.js.map +1 -0
  116. package/dist/index.umd.js +17021 -0
  117. package/dist/index.umd.js.map +1 -0
  118. package/dist/internal/common.d.ts +14 -0
  119. package/dist/internal/useRestoreScroll.d.ts +15 -0
  120. package/dist/locales/de.d.ts +2 -0
  121. package/dist/locales/en.d.ts +10 -0
  122. package/dist/locales/es.d.ts +10 -0
  123. package/dist/locales/fr.d.ts +2 -0
  124. package/dist/locales/hi.d.ts +2 -0
  125. package/dist/locales/it.d.ts +2 -0
  126. package/dist/locales/pt.d.ts +7 -0
  127. package/dist/util/constants.d.ts +1 -0
  128. package/dist/util/createFormexStub.d.ts +2 -0
  129. package/dist/util/entity_cache.d.ts +22 -0
  130. package/dist/util/enums.d.ts +5 -0
  131. package/dist/util/icon_list.d.ts +5 -0
  132. package/dist/util/icons.d.ts +20 -0
  133. package/dist/util/index.d.ts +8 -0
  134. package/dist/util/previews.d.ts +4 -0
  135. package/dist/util/useStorageUploadController.d.ts +44 -0
  136. package/dist/vitePlugin.d.ts +60 -0
  137. package/dist/vitePlugin.js +113 -0
  138. package/package.json +145 -0
  139. package/src/auth/api.ts +148 -0
  140. package/src/auth/index.ts +25 -0
  141. package/src/auth/types.ts +115 -0
  142. package/src/auth/useRebaseAuthController.ts +328 -0
  143. package/src/components/AIIcon.tsx +47 -0
  144. package/src/components/AuthSimulationMenu.tsx +0 -0
  145. package/src/components/ConfirmationDialog.tsx +48 -0
  146. package/src/components/Debug/UIReferenceView.tsx +958 -0
  147. package/src/components/Debug/UIStyleGuide.tsx +156 -0
  148. package/src/components/Debug/collection-views/CardViewDemo.tsx +85 -0
  149. package/src/components/Debug/collection-views/CollectionTableDemo.tsx +107 -0
  150. package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +90 -0
  151. package/src/components/Debug/collection-views/index.ts +3 -0
  152. package/src/components/Debug/collection-views/sample_data.ts +42 -0
  153. package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +389 -0
  154. package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +1102 -0
  155. package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +133 -0
  156. package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +290 -0
  157. package/src/components/Debug/crm-dashboard/RecentActivity.tsx +215 -0
  158. package/src/components/Debug/crm-dashboard/TaskFilters.tsx +184 -0
  159. package/src/components/Debug/crm-dashboard/TaskTable.tsx +427 -0
  160. package/src/components/Debug/crm-dashboard/TasksView.tsx +234 -0
  161. package/src/components/ErrorTooltip.tsx +12 -0
  162. package/src/components/ErrorView.tsx +94 -0
  163. package/src/components/LanguageToggle.tsx +59 -0
  164. package/src/components/LoginView/LoginView.tsx +932 -0
  165. package/src/components/LoginView/index.ts +2 -0
  166. package/src/components/NotFoundPage.tsx +27 -0
  167. package/src/components/RebaseAuth.tsx +27 -0
  168. package/src/components/RebaseLogo.tsx +29 -0
  169. package/src/components/SchemaDriftBanner.tsx +102 -0
  170. package/src/components/UnsavedChangesDialog.tsx +46 -0
  171. package/src/components/UserDisplay.tsx +54 -0
  172. package/src/components/UserSelectPopover.tsx +399 -0
  173. package/src/components/UserSettingsView.tsx +318 -0
  174. package/src/components/common/index.ts +6 -0
  175. package/src/components/common/table_height.tsx +21 -0
  176. package/src/components/common/types.tsx +66 -0
  177. package/src/components/common/useColumnsIds.tsx +222 -0
  178. package/src/components/common/useDataTableController.tsx +514 -0
  179. package/src/components/common/useDebouncedData.ts +49 -0
  180. package/src/components/common/useScrollRestoration.tsx +68 -0
  181. package/src/components/index.tsx +26 -0
  182. package/src/contexts/AdminModeController.tsx +11 -0
  183. package/src/contexts/AnalyticsContext.tsx +4 -0
  184. package/src/contexts/AuthControllerContext.tsx +4 -0
  185. package/src/contexts/CollectionScopeContext.tsx +67 -0
  186. package/src/contexts/ComponentOverrideContext.tsx +81 -0
  187. package/src/contexts/CustomizationControllerContext.tsx +4 -0
  188. package/src/contexts/DataDriverContext.tsx +4 -0
  189. package/src/contexts/DataSourcesContext.tsx +36 -0
  190. package/src/contexts/DatabaseAdminContext.tsx +4 -0
  191. package/src/contexts/DialogsProvider.tsx +56 -0
  192. package/src/contexts/EffectiveRoleController.tsx +12 -0
  193. package/src/contexts/ModeController.tsx +11 -0
  194. package/src/contexts/RebaseClientInstanceContext.tsx +7 -0
  195. package/src/contexts/RebaseDataContext.tsx +4 -0
  196. package/src/contexts/SnackbarProvider.tsx +15 -0
  197. package/src/contexts/StorageSourceContext.tsx +4 -0
  198. package/src/contexts/StorageSourcesContext.tsx +30 -0
  199. package/src/contexts/UserConfigurationPersistenceContext.tsx +4 -0
  200. package/src/contexts/index.ts +17 -0
  201. package/src/core/PluginLifecycleManager.tsx +95 -0
  202. package/src/core/PluginProviderStack.tsx +40 -0
  203. package/src/core/Rebase.tsx +439 -0
  204. package/src/core/RebaseProps.tsx +320 -0
  205. package/src/core/RebaseRouter.tsx +17 -0
  206. package/src/core/RebaseRoutes.tsx +24 -0
  207. package/src/core/index.tsx +5 -0
  208. package/src/hooks/ApiConfigContext.tsx +41 -0
  209. package/src/hooks/data/delete.ts +55 -0
  210. package/src/hooks/data/save.ts +77 -0
  211. package/src/hooks/data/useCollection.tsx +176 -0
  212. package/src/hooks/data/useData.tsx +18 -0
  213. package/src/hooks/data/useDataOrder.ts +26 -0
  214. package/src/hooks/data/useFetch.tsx +140 -0
  215. package/src/hooks/data/useRelationSelector.tsx +234 -0
  216. package/src/hooks/index.tsx +45 -0
  217. package/src/hooks/useAdminModeController.tsx +23 -0
  218. package/src/hooks/useAnalyticsController.tsx +8 -0
  219. package/src/hooks/useAuthController.tsx +14 -0
  220. package/src/hooks/useAuthSubscription.ts +87 -0
  221. package/src/hooks/useBackendStorageSource.ts +288 -0
  222. package/src/hooks/useBridgeRegistration.tsx +32 -0
  223. package/src/hooks/useBrowserTitleAndIcon.tsx +23 -0
  224. package/src/hooks/useBuildAdminModeController.tsx +24 -0
  225. package/src/hooks/useBuildEffectiveRoleController.tsx +31 -0
  226. package/src/hooks/useBuildLocalConfigurationPersistence.tsx +75 -0
  227. package/src/hooks/useBuildModeController.tsx +71 -0
  228. package/src/hooks/useClipboard.tsx +158 -0
  229. package/src/hooks/useCollapsedGroups.ts +116 -0
  230. package/src/hooks/useComponentOverride.tsx +59 -0
  231. package/src/hooks/useCustomizationController.tsx +14 -0
  232. package/src/hooks/useDialogsController.tsx +15 -0
  233. package/src/hooks/useEffectiveRoleController.tsx +10 -0
  234. package/src/hooks/useLargeLayout.tsx +65 -0
  235. package/src/hooks/useModeController.tsx +23 -0
  236. package/src/hooks/usePermissions.ts +44 -0
  237. package/src/hooks/useRebaseClient.tsx +10 -0
  238. package/src/hooks/useRebaseContext.tsx +83 -0
  239. package/src/hooks/useRebaseRegistry.tsx +91 -0
  240. package/src/hooks/useResolvedComponent.tsx +159 -0
  241. package/src/hooks/useSlot.tsx +64 -0
  242. package/src/hooks/useSnackbarController.tsx +52 -0
  243. package/src/hooks/useStorageSource.tsx +11 -0
  244. package/src/hooks/useStudioBridge.tsx +209 -0
  245. package/src/hooks/useTranslation.ts +33 -0
  246. package/src/hooks/useUnsavedChangesDialog.tsx +64 -0
  247. package/src/hooks/useUserConfigurationPersistence.tsx +11 -0
  248. package/src/i18n/RebaseI18nProvider.tsx +160 -0
  249. package/src/index.ts +25 -0
  250. package/src/internal/common.tsx +16 -0
  251. package/src/internal/useRestoreScroll.tsx +69 -0
  252. package/src/locales/de.ts +902 -0
  253. package/src/locales/en.ts +980 -0
  254. package/src/locales/es.ts +922 -0
  255. package/src/locales/fr.ts +901 -0
  256. package/src/locales/hi.ts +901 -0
  257. package/src/locales/it.ts +901 -0
  258. package/src/locales/pt.ts +909 -0
  259. package/src/util/constants.ts +7 -0
  260. package/src/util/createFormexStub.tsx +66 -0
  261. package/src/util/entity_cache.ts +239 -0
  262. package/src/util/enums.ts +32 -0
  263. package/src/util/icon_list.ts +22 -0
  264. package/src/util/icons.tsx +149 -0
  265. package/src/util/index.ts +12 -0
  266. package/src/util/previews.ts +105 -0
  267. package/src/util/useStorageUploadController.tsx +350 -0
  268. package/src/vitePlugin.ts +196 -0
@@ -0,0 +1,932 @@
1
+ import React, { ReactNode, useEffect, useRef, useState } from "react";
2
+
3
+ /** Google Identity Services SDK — injected by the GIS <script> tag. */
4
+ declare global {
5
+ interface Window {
6
+ google?: {
7
+ accounts: {
8
+ oauth2: {
9
+ initCodeClient(config: {
10
+ client_id: string;
11
+ scope: string;
12
+ ux_mode: "popup" | "redirect";
13
+ callback: (response: { code?: string; error?: string }) => void;
14
+ }): { requestCode(): void };
15
+ };
16
+ };
17
+ };
18
+ }
19
+ }
20
+
21
+ import {
22
+ ArrowLeftIcon,
23
+ Button,
24
+ cls,
25
+ IconButton,
26
+ iconSize,
27
+ LoadingButton,
28
+ MailIcon,
29
+ Menu,
30
+ MenuItem,
31
+ MoonIcon,
32
+ SunIcon,
33
+ SunMoonIcon,
34
+ TextField,
35
+ Typography
36
+ } from "@rebasepro/ui";
37
+ import { AuthControllerExtended, User } from "@rebasepro/types";
38
+ import { ErrorView } from "../ErrorView";
39
+ import { RebaseLogo } from "../RebaseLogo";
40
+ import { LanguageToggle } from "../LanguageToggle";
41
+ import { useModeController, useTranslation } from "../../hooks";
42
+
43
+ /**
44
+ * Props for the generic LoginView.
45
+ * Consumes {@link AuthControllerExtended} — works with any backend
46
+ * (Rebase custom backend, Firebase, etc.)
47
+ */
48
+ export interface LoginViewProps {
49
+ /**
50
+ * Auth controller — must implement AuthControllerExtended.
51
+ */
52
+ authController: AuthControllerExtended;
53
+
54
+ /**
55
+ * Path to the logo displayed in the login screen
56
+ */
57
+ logo?: string;
58
+
59
+ /**
60
+ * Enable the skip login button
61
+ */
62
+ allowSkipLogin?: boolean;
63
+
64
+ /**
65
+ * Disable the login buttons
66
+ */
67
+ disabled?: boolean;
68
+
69
+ /**
70
+ * Prevent users from creating new accounts.
71
+ * If not set, checks `authController.capabilities.registration`.
72
+ */
73
+ disableSignupScreen?: boolean;
74
+
75
+ /**
76
+ * Display this component when no user is found
77
+ */
78
+ noUserComponent?: ReactNode;
79
+
80
+ /**
81
+ * Display this component below the sign-in buttons
82
+ */
83
+ additionalComponent?: ReactNode;
84
+
85
+ /**
86
+ * Display this component above the sign-in buttons
87
+ */
88
+ topComponent?: ReactNode;
89
+
90
+ /**
91
+ * Error message when user is not allowed access
92
+ */
93
+ notAllowedError?: string | Error;
94
+
95
+
96
+ /**
97
+ * Google client ID for Google OAuth.
98
+ * Required when Google login is enabled via ID token flow.
99
+ */
100
+ googleClientId?: string;
101
+
102
+ /**
103
+ * GitHub client ID for GitHub OAuth.
104
+ */
105
+ githubClientId?: string;
106
+
107
+ /**
108
+ * LinkedIn client ID for LinkedIn OAuth.
109
+ */
110
+ linkedinClientId?: string;
111
+
112
+ /**
113
+ * Optional custom title shown above options
114
+ */
115
+ title?: string;
116
+
117
+ /**
118
+ * Optional custom subtitle shown above options
119
+ */
120
+ subtitle?: string;
121
+
122
+ /**
123
+ * When true, shows bootstrap/setup UI (first-user creation).
124
+ * If not set, derived from `authController` if it exposes `needsSetup`.
125
+ */
126
+ needsSetup?: boolean;
127
+
128
+ /**
129
+ * Whether registration is enabled.
130
+ * If not set, derived from `authController.capabilities.registration`.
131
+ */
132
+ registrationEnabled?: boolean;
133
+
134
+ /**
135
+ * Pre-fill the email field (e.g. for demo or testing environments).
136
+ */
137
+ defaultEmail?: string;
138
+
139
+ /**
140
+ * Pre-fill the password field (e.g. for demo or testing environments).
141
+ */
142
+ defaultPassword?: string;
143
+ }
144
+
145
+ type AuthMode = "buttons" | "login" | "register" | "forgot";
146
+
147
+ /**
148
+ * The shared field background mixin (`dark:bg-black/30`) is invisible on the
149
+ * near-black login card, so login inputs get an explicit border in dark mode.
150
+ */
151
+ const loginFieldClasses = "w-full dark:border dark:border-surface-700/70 dark:hover:border-surface-600";
152
+
153
+ /**
154
+ * Generic login view component that works with any AuthControllerExtended.
155
+ * Feature-detects capabilities to show/hide login methods.
156
+ * @group Core
157
+ */
158
+ export function LoginView({
159
+ logo,
160
+ authController,
161
+ noUserComponent,
162
+ disableSignupScreen = false,
163
+ disabled = false,
164
+ notAllowedError,
165
+ googleClientId,
166
+ githubClientId,
167
+ linkedinClientId,
168
+ title,
169
+ subtitle,
170
+ needsSetup,
171
+ registrationEnabled,
172
+ additionalComponent,
173
+ topComponent,
174
+ defaultEmail,
175
+ defaultPassword
176
+ }: LoginViewProps) {
177
+
178
+ const modeState = useModeController();
179
+ const { mode: colorMode, setMode: setColorMode } = modeState;
180
+ const { t } = useTranslation();
181
+
182
+ const [mode, setMode] = useState<AuthMode>("buttons");
183
+ const [fadeIn, setFadeIn] = useState(false);
184
+ const [viewVisible, setViewVisible] = useState(true);
185
+
186
+ const switchMode = (newMode: AuthMode) => {
187
+ setViewVisible(false);
188
+ setTimeout(() => {
189
+ setMode(newMode);
190
+ setViewVisible(true);
191
+ }, 150);
192
+ };
193
+
194
+ // Resolve capabilities — explicit props override authController.capabilities
195
+ const caps = authController.capabilities ?? {};
196
+ const isBootstrapMode = needsSetup
197
+ ?? ("needsSetup" in authController && !!(authController as { needsSetup?: boolean }).needsSetup)
198
+ ?? false;
199
+ const canRegister = registrationEnabled ?? caps.registration ?? false;
200
+ const hasGoogleLogin = googleClientId && (caps.enabledProviders?.includes("google") ?? caps.googleLogin ?? false);
201
+ const hasGitHubLogin = githubClientId && (caps.enabledProviders?.includes("github") ?? false);
202
+ const hasLinkedinLogin = linkedinClientId && (caps.enabledProviders?.includes("linkedin") ?? false);
203
+ const hasPasswordReset = caps.passwordReset ?? !!authController.forgotPassword;
204
+
205
+ const showRegistration = !disableSignupScreen && canRegister;
206
+
207
+ useEffect(() => {
208
+ const timer = setTimeout(() => setFadeIn(true), 50);
209
+ return () => clearTimeout(timer);
210
+ }, []);
211
+
212
+ // Effect to handle incoming redirect OAuth codes (GitHub, LinkedIn, etc.)
213
+ useEffect(() => {
214
+ const params = new URLSearchParams(window.location.search);
215
+ const code = params.get("code");
216
+ const provider = localStorage.getItem("rebase_oauth_provider");
217
+ if (code && provider) {
218
+ localStorage.removeItem("rebase_oauth_provider");
219
+ // Clear URL search params without page reload
220
+ const cleanUrl = window.location.origin + window.location.pathname;
221
+ window.history.replaceState({}, document.title, cleanUrl);
222
+
223
+ if (authController.oauthLogin) {
224
+ authController.oauthLogin(provider, {
225
+ code,
226
+ redirectUri: cleanUrl
227
+ }).catch((err) => {
228
+ console.error(`${provider} login failed:`, err);
229
+ });
230
+ }
231
+ }
232
+ }, [authController]);
233
+
234
+
235
+
236
+ let logoComponent;
237
+ if (logo) {
238
+ logoComponent = <img src={logo}
239
+ style={{
240
+ height: "100%",
241
+ width: "100%",
242
+ objectFit: "cover"
243
+ }}
244
+ alt={"Logo"}/>;
245
+ } else {
246
+ logoComponent = <RebaseLogo/>;
247
+ }
248
+
249
+ let notAllowedMessage: string | undefined;
250
+ if (notAllowedError) {
251
+ if (typeof notAllowedError === "string") {
252
+ notAllowedMessage = notAllowedError;
253
+ } else if (notAllowedError instanceof Error) {
254
+ notAllowedMessage = notAllowedError.message;
255
+ } else {
256
+ notAllowedMessage = "It looks like you don't have access, based on the specified access configuration";
257
+ }
258
+ }
259
+
260
+ return (
261
+ <div
262
+ className={cls(
263
+ "relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-surface-50 dark:bg-surface-900 overflow-hidden",
264
+ fadeIn ? "opacity-100" : "opacity-0"
265
+ )}>
266
+
267
+ {/* Top-right controls */}
268
+ <div className="absolute top-4 right-4 flex items-center gap-1 z-10">
269
+ <LanguageToggle/>
270
+ <Menu
271
+ trigger={<IconButton
272
+ color="inherit"
273
+ aria-label="Toggle theme">
274
+ {colorMode === "dark"
275
+ ? <MoonIcon size={iconSize.small}/>
276
+ : <SunIcon size={iconSize.small}/>}
277
+ </IconButton>}>
278
+ <MenuItem onClick={() => setColorMode("dark")}><MoonIcon size={iconSize.smallest}/> {t("dark_mode")}</MenuItem>
279
+ <MenuItem onClick={() => setColorMode("light")}><SunIcon size={iconSize.smallest}/> {t("light_mode")}</MenuItem>
280
+ <MenuItem onClick={() => setColorMode("system")}><SunMoonIcon size={iconSize.smallest}/> {t("system_mode")}</MenuItem>
281
+ </Menu>
282
+ </div>
283
+
284
+ <div className="relative flex flex-col items-center w-[440px] max-w-full p-8 sm:p-10 bg-white/70 dark:bg-surface-800/70 backdrop-blur-xl border border-surface-200/50 dark:border-surface-700/50 rounded-2xl shadow-2xl z-10 transition-all duration-300 hover:shadow-primary-500/5">
285
+ {/* Logo */}
286
+ <div className="w-24 h-24 m-2 mb-4 drop-shadow-md">
287
+ {logoComponent}
288
+ </div>
289
+
290
+ {notAllowedMessage && (
291
+ <div className="p-4 w-full">
292
+ <ErrorView error={notAllowedMessage}/>
293
+ </div>
294
+ )}
295
+
296
+ <div className={cls(
297
+ "w-full transition-opacity duration-150",
298
+ viewVisible ? "opacity-100" : "opacity-0"
299
+ )}>
300
+
301
+ {/* Bootstrap mode: show setup form directly */}
302
+ {isBootstrapMode && !authController.user && (
303
+ <LoginForm
304
+ authController={authController}
305
+ registrationMode={true}
306
+ onClose={() => {}}
307
+ onForgotPassword={() => {}}
308
+ disableSignupScreen={false}
309
+ bootstrapMode={true}
310
+ defaultEmail={defaultEmail}
311
+ defaultPassword={defaultPassword}
312
+ />
313
+ )}
314
+
315
+ {/* Normal mode */}
316
+ {!isBootstrapMode && (
317
+ <>
318
+ {/* Provider buttons screen */}
319
+ {mode === "buttons" && (
320
+ <div className="w-full flex flex-col gap-3 mt-2">
321
+ {(title || subtitle) && (
322
+ <div className="text-center mb-2">
323
+ {title && (
324
+ <Typography variant="h6" className="mb-0.5 font-bold">
325
+ {title}
326
+ </Typography>
327
+ )}
328
+ {subtitle && (
329
+ <Typography variant="body2" color="secondary" className="mb-4">
330
+ {subtitle}
331
+ </Typography>
332
+ )}
333
+ </div>
334
+ )}
335
+ {topComponent && (
336
+ <div className="w-full">
337
+ {topComponent}
338
+ </div>
339
+ )}
340
+ <LoginButton
341
+ disabled={disabled}
342
+ text={"Sign in with email"}
343
+ icon={<MailIcon/>}
344
+ onClick={() => switchMode("login")}
345
+ />
346
+ {hasGoogleLogin && googleClientId && (
347
+ <GoogleLoginButton
348
+ disabled={disabled}
349
+ googleClientId={googleClientId}
350
+ authController={authController}
351
+ />
352
+ )}
353
+ {hasGitHubLogin && githubClientId && (
354
+ <GitHubLoginButton
355
+ disabled={disabled}
356
+ githubClientId={githubClientId}
357
+ />
358
+ )}
359
+ {hasLinkedinLogin && linkedinClientId && (
360
+ <LinkedInLoginButton
361
+ disabled={disabled}
362
+ linkedinClientId={linkedinClientId}
363
+ />
364
+ )}
365
+ {showRegistration && (
366
+ <div className="mt-2 text-center">
367
+ <Typography variant="body2" color="secondary">
368
+ Don&apos;t have an account?{" "}
369
+ <button
370
+ type="button"
371
+ className="font-semibold hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
372
+ onClick={() => switchMode("register")}
373
+ >
374
+ Create one
375
+ </button>
376
+ </Typography>
377
+ </div>
378
+ )}
379
+ </div>
380
+ )}
381
+
382
+ {/* Login form */}
383
+ {mode === "login" && (
384
+ <LoginForm
385
+ authController={authController}
386
+ registrationMode={false}
387
+ onClose={() => switchMode("buttons")}
388
+ onForgotPassword={hasPasswordReset ? () => switchMode("forgot") : undefined}
389
+ noUserComponent={noUserComponent}
390
+ disableSignupScreen={disableSignupScreen}
391
+ switchToRegister={showRegistration ? () => switchMode("register") : undefined}
392
+ defaultEmail={defaultEmail}
393
+ defaultPassword={defaultPassword}
394
+ />
395
+ )}
396
+
397
+ {/* Registration form */}
398
+ {mode === "register" && (
399
+ <LoginForm
400
+ authController={authController}
401
+ registrationMode={true}
402
+ onClose={() => switchMode("buttons")}
403
+ onForgotPassword={hasPasswordReset ? () => switchMode("forgot") : undefined}
404
+ noUserComponent={noUserComponent}
405
+ disableSignupScreen={disableSignupScreen}
406
+ switchToLogin={() => switchMode("login")}
407
+ defaultEmail={defaultEmail}
408
+ defaultPassword={defaultPassword}
409
+ />
410
+ )}
411
+
412
+ {/* Forgot password form */}
413
+ {mode === "forgot" && authController.forgotPassword && (
414
+ <ForgotPasswordForm
415
+ authController={authController}
416
+ onClose={() => switchMode("login")}
417
+ />
418
+ )}
419
+ </>
420
+ )}
421
+ </div>
422
+
423
+ {additionalComponent && !isBootstrapMode && mode === "buttons" && (
424
+ <div className="w-full">
425
+ {additionalComponent}
426
+ </div>
427
+ )}
428
+ </div>
429
+ </div>
430
+ );
431
+ }
432
+
433
+ function LoginButton({
434
+ icon,
435
+ onClick,
436
+ text,
437
+ disabled
438
+ }: { icon: React.ReactNode, onClick: () => void, text: string, disabled?: boolean }) {
439
+ return (
440
+ <Button
441
+ disabled={disabled}
442
+ className="w-full transition-transform duration-200 active:scale-[0.98]"
443
+ variant="outlined"
444
+ size="large"
445
+ onClick={onClick}>
446
+ <div className="flex items-center justify-center w-full gap-3 py-1">
447
+ <span className="flex items-center justify-center w-5 h-5">
448
+ {icon}
449
+ </span>
450
+ <Typography variant="button">{text}</Typography>
451
+ </div>
452
+ </Button>
453
+ );
454
+ }
455
+
456
+ const GoogleIcon = () => (
457
+ <svg viewBox="0 0 24 24" width="20" height="20">
458
+ <path fill="#4285F4"
459
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
460
+ <path fill="#34A853"
461
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
462
+ <path fill="#FBBC05"
463
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
464
+ <path fill="#EA4335"
465
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
466
+ </svg>
467
+ );
468
+
469
+ function GoogleLoginButton({
470
+ disabled,
471
+ googleClientId,
472
+ authController
473
+ }: {
474
+ disabled?: boolean,
475
+ googleClientId: string,
476
+ authController: AuthControllerExtended
477
+ }) {
478
+ const codeClientRef = useRef<{ requestCode(): void } | null>(null);
479
+
480
+ useEffect(() => {
481
+ if (!authController.googleLogin) return;
482
+
483
+ const google = window.google;
484
+ if (!google || codeClientRef.current) return;
485
+
486
+ codeClientRef.current = google.accounts.oauth2.initCodeClient({
487
+ client_id: googleClientId,
488
+ scope: "openid email profile",
489
+ ux_mode: "popup",
490
+ callback: async (response: { code?: string; error?: string }) => {
491
+ if (response.error || !response.code) {
492
+ console.error("Google login error:", response.error);
493
+ return;
494
+ }
495
+ try {
496
+ // Send the authorization code to the backend.
497
+ // redirectUri "postmessage" is required when using popup ux_mode.
498
+ await authController.googleLogin!({
499
+ code: response.code,
500
+ redirectUri: "postmessage"
501
+ });
502
+ } catch (err: unknown) {
503
+ console.error("Google login error:", err);
504
+ }
505
+ }
506
+ });
507
+ }, [googleClientId, authController]);
508
+
509
+ const handleClick = () => {
510
+ if (!codeClientRef.current) {
511
+ console.error("Google Sign-In not loaded");
512
+ return;
513
+ }
514
+ codeClientRef.current.requestCode();
515
+ };
516
+
517
+ return (
518
+ <LoginButton
519
+ disabled={disabled}
520
+ text="Sign in with Google"
521
+ icon={<GoogleIcon/>}
522
+ onClick={handleClick}
523
+ />
524
+ );
525
+ }
526
+
527
+ const GitHubIcon = () => (
528
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
529
+ <path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.579.688.481C19.137 20.162 22 16.418 22 12c0-5.523-4.477-10-10-10z"/>
530
+ </svg>
531
+ );
532
+
533
+ function GitHubLoginButton({
534
+ disabled,
535
+ githubClientId
536
+ }: {
537
+ disabled?: boolean,
538
+ githubClientId: string
539
+ }) {
540
+ const handleClick = () => {
541
+ localStorage.setItem("rebase_oauth_provider", "github");
542
+ const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
543
+ const scope = "read:user,user:email";
544
+ window.location.href = `https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
545
+ };
546
+
547
+ return (
548
+ <LoginButton
549
+ disabled={disabled}
550
+ text="Sign in with GitHub"
551
+ icon={<GitHubIcon/>}
552
+ onClick={handleClick}
553
+ />
554
+ );
555
+ }
556
+
557
+ const LinkedInIcon = () => (
558
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
559
+ <path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
560
+ </svg>
561
+ );
562
+
563
+ function LinkedInLoginButton({
564
+ disabled,
565
+ linkedinClientId
566
+ }: {
567
+ disabled?: boolean,
568
+ linkedinClientId: string
569
+ }) {
570
+ const handleClick = () => {
571
+ localStorage.setItem("rebase_oauth_provider", "linkedin");
572
+ const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
573
+ const scope = "openid profile email";
574
+ window.location.href = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${linkedinClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
575
+ };
576
+
577
+ return (
578
+ <LoginButton
579
+ disabled={disabled}
580
+ text="Sign in with LinkedIn"
581
+ icon={<LinkedInIcon/>}
582
+ onClick={handleClick}
583
+ />
584
+ );
585
+ }
586
+
587
+ function LoginForm({
588
+ onClose,
589
+ onForgotPassword,
590
+ authController,
591
+ registrationMode,
592
+ noUserComponent,
593
+ disableSignupScreen,
594
+ bootstrapMode = false,
595
+ switchToRegister,
596
+ switchToLogin,
597
+ defaultEmail,
598
+ defaultPassword
599
+ }: {
600
+ onClose: () => void,
601
+ onForgotPassword?: () => void,
602
+ authController: AuthControllerExtended,
603
+ registrationMode: boolean,
604
+ noUserComponent?: ReactNode,
605
+ disableSignupScreen: boolean,
606
+ bootstrapMode?: boolean,
607
+ switchToRegister?: () => void,
608
+ switchToLogin?: () => void,
609
+ defaultEmail?: string,
610
+ defaultPassword?: string
611
+ }) {
612
+ const passwordRef = useRef<HTMLInputElement | null>(null);
613
+
614
+ const [email, setEmail] = useState<string | undefined>(defaultEmail);
615
+ const [password, setPassword] = useState<string | undefined>(defaultPassword);
616
+ const [displayName, setDisplayName] = useState<string>();
617
+
618
+ useEffect(() => {
619
+ if (!document) return;
620
+ const escFunction = (event: KeyboardEvent) => {
621
+ if (event.key === "Escape") {
622
+ onClose();
623
+ }
624
+ };
625
+ document.addEventListener("keydown", escFunction, false);
626
+ return () => {
627
+ document.removeEventListener("keydown", escFunction, false);
628
+ };
629
+ }, [onClose]);
630
+
631
+ function handleEnterPassword() {
632
+ if (email && password && authController.emailPasswordLogin) {
633
+ authController.emailPasswordLogin(email, password);
634
+ }
635
+ }
636
+
637
+ function handleRegistration() {
638
+ if (email && password && authController.register) {
639
+ authController.register(email, password, displayName);
640
+ }
641
+ }
642
+
643
+ const handleSubmit = (event: React.FormEvent) => {
644
+ event.preventDefault();
645
+ if (registrationMode)
646
+ handleRegistration();
647
+ else
648
+ handleEnterPassword();
649
+ };
650
+
651
+ const title = bootstrapMode
652
+ ? "Welcome!"
653
+ : registrationMode
654
+ ? "Create account"
655
+ : "Sign in";
656
+
657
+ const subtitle = bootstrapMode
658
+ ? "Create your admin account to get started. This account will have admin privileges."
659
+ : registrationMode
660
+ ? "Fill in your details to create a new account"
661
+ : "Enter your credentials to continue";
662
+
663
+ const buttonLabel = registrationMode ? "Create account" : "Sign in";
664
+
665
+ return (
666
+ <form onSubmit={handleSubmit} className="flex flex-col w-full gap-1 mt-2">
667
+ {!bootstrapMode && (
668
+ <div className="w-full mb-2 -ml-2.5">
669
+ <IconButton onClick={onClose}>
670
+ <ArrowLeftIcon/>
671
+ </IconButton>
672
+ </div>
673
+ )}
674
+
675
+ {(() => {
676
+ const err = authController.authProviderError;
677
+ if (!err || authController.user) return null;
678
+ const msg: string = err instanceof Error ? err.message : String(err);
679
+ return (
680
+ <div className="w-full mb-2">
681
+ <ErrorView error={msg}/>
682
+ </div>
683
+ );
684
+ })()}
685
+
686
+ <Typography variant="h6" className="mb-0.5">
687
+ {title}
688
+ </Typography>
689
+ <Typography variant="body2" color="secondary" className="mb-5">
690
+ {subtitle}
691
+ </Typography>
692
+
693
+ {registrationMode && !bootstrapMode && noUserComponent && (
694
+ <div className="w-full mb-2">
695
+ {noUserComponent}
696
+ </div>
697
+ )}
698
+
699
+ {registrationMode && (
700
+ <div className="w-full mb-3">
701
+ <Typography variant="label" color="secondary" className="mb-1">
702
+ Display Name
703
+ </Typography>
704
+ <TextField placeholder="Jane Doe (optional)"
705
+ className={loginFieldClasses}
706
+ value={displayName ?? ""}
707
+ disabled={authController.initialLoading}
708
+ type="text"
709
+ size="medium"
710
+ onChange={(event) => setDisplayName(event.target.value)}/>
711
+ </div>
712
+ )}
713
+
714
+ <div className="w-full mb-3">
715
+ <Typography variant="label" color="secondary" className="mb-1">
716
+ Email
717
+ </Typography>
718
+ <TextField placeholder="you@example.com"
719
+ className={loginFieldClasses}
720
+ autoFocus
721
+ value={email ?? ""}
722
+ disabled={authController.initialLoading}
723
+ type="email"
724
+ size="medium"
725
+ onChange={(event) => setEmail(event.target.value)}/>
726
+ </div>
727
+
728
+ <div className="w-full mb-1">
729
+ <Typography variant="label" color="secondary" className="mb-1">
730
+ Password
731
+ </Typography>
732
+ <TextField placeholder="••••••••"
733
+ className={loginFieldClasses}
734
+ value={password ?? ""}
735
+ disabled={authController.initialLoading}
736
+ inputRef={passwordRef}
737
+ type="password"
738
+ size="medium"
739
+ onChange={(event) => setPassword(event.target.value)}/>
740
+ </div>
741
+
742
+ {registrationMode && (
743
+ <Typography variant="caption" color="secondary" className="mb-3">
744
+ Password must be 8+ characters with uppercase, lowercase, and a number
745
+ </Typography>
746
+ )}
747
+
748
+ {!registrationMode && onForgotPassword && (
749
+ <div className="w-full text-right mb-3">
750
+ <button
751
+ type="button"
752
+ className="text-xs font-medium hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
753
+ onClick={onForgotPassword}
754
+ >
755
+ Forgot password?
756
+ </button>
757
+ </div>
758
+ )}
759
+
760
+ <LoadingButton
761
+ type="submit"
762
+ variant="filled"
763
+ color="primary"
764
+ className="w-full mt-1"
765
+ size="large"
766
+ loading={authController.authLoading}
767
+ disabled={authController.authLoading || !email || !password}
768
+ >
769
+ {buttonLabel}
770
+ </LoadingButton>
771
+
772
+ {/* Switch between login/register */}
773
+ {switchToRegister && (
774
+ <div className="mt-4 text-center">
775
+ <Typography variant="body2" color="secondary">
776
+ Don&apos;t have an account?{" "}
777
+ <button
778
+ type="button"
779
+ className="font-semibold hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
780
+ onClick={switchToRegister}
781
+ >
782
+ Create one
783
+ </button>
784
+ </Typography>
785
+ </div>
786
+ )}
787
+
788
+ {switchToLogin && (
789
+ <div className="mt-4 text-center">
790
+ <Typography variant="body2" color="secondary">
791
+ Already have an account?{" "}
792
+ <button
793
+ type="button"
794
+ className="font-semibold hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
795
+ onClick={switchToLogin}
796
+ >
797
+ Sign in
798
+ </button>
799
+ </Typography>
800
+ </div>
801
+ )}
802
+ </form>
803
+ );
804
+ }
805
+
806
+ function ForgotPasswordForm({
807
+ onClose,
808
+ authController
809
+ }: {
810
+ onClose: () => void,
811
+ authController: AuthControllerExtended
812
+ }) {
813
+ const [email, setEmail] = useState<string>("");
814
+ const [submitted, setSubmitted] = useState(false);
815
+ const [error, setError] = useState<string | null>(null);
816
+
817
+ useEffect(() => {
818
+ if (!document) return;
819
+ const escFunction = (event: KeyboardEvent) => {
820
+ if (event.key === "Escape") {
821
+ onClose();
822
+ }
823
+ };
824
+ document.addEventListener("keydown", escFunction, false);
825
+ return () => {
826
+ document.removeEventListener("keydown", escFunction, false);
827
+ };
828
+ }, [onClose]);
829
+
830
+ const handleSubmit = async (event: React.FormEvent) => {
831
+ event.preventDefault();
832
+ setError(null);
833
+
834
+ if (!email) {
835
+ setError("Please enter your email address");
836
+ return;
837
+ }
838
+
839
+ if (!authController.forgotPassword) {
840
+ setError("Password reset is not available with this auth provider.");
841
+ return;
842
+ }
843
+
844
+ try {
845
+ await authController.forgotPassword(email);
846
+ setSubmitted(true);
847
+ } catch (err: unknown) {
848
+ // Check for EMAIL_NOT_CONFIGURED error
849
+ if (err instanceof Error && (err as { code?: string }).code === "EMAIL_NOT_CONFIGURED") {
850
+ setError("Password reset is not available. Please contact your administrator.");
851
+ } else {
852
+ // Still show success (security: don't reveal if email exists)
853
+ setSubmitted(true);
854
+ }
855
+ }
856
+ };
857
+
858
+ if (submitted) {
859
+ return (
860
+ <div className="flex flex-col w-full gap-4 mt-2">
861
+ <div className="w-full -ml-2.5">
862
+ <IconButton onClick={onClose}>
863
+ <ArrowLeftIcon/>
864
+ </IconButton>
865
+ </div>
866
+
867
+ <div className="text-center rounded-xl p-6 bg-surface-50 dark:bg-surface-900/60 dark:border dark:border-surface-700/50">
868
+ <div className="text-3xl mb-3">📧</div>
869
+ <Typography variant="subtitle1" className="mb-2">
870
+ Check your email
871
+ </Typography>
872
+ <Typography variant="body2" color="secondary">
873
+ If an account exists for <strong>{email}</strong>, you&apos;ll receive a password reset link shortly.
874
+ </Typography>
875
+ </div>
876
+
877
+ <Button onClick={onClose} variant="text" className="mt-2">
878
+ Back to sign in
879
+ </Button>
880
+ </div>
881
+ );
882
+ }
883
+
884
+ return (
885
+ <form onSubmit={handleSubmit} className="flex flex-col w-full gap-1 mt-2">
886
+ <div className="w-full mb-2 -ml-2.5">
887
+ <IconButton onClick={onClose}>
888
+ <ArrowLeftIcon/>
889
+ </IconButton>
890
+ </div>
891
+
892
+ <Typography variant="h6" className="mb-0.5">
893
+ Reset password
894
+ </Typography>
895
+ <Typography variant="body2" color="secondary" className="mb-5">
896
+ Enter your email and we&apos;ll send you a reset link.
897
+ </Typography>
898
+
899
+ {error && (
900
+ <div className="w-full mb-3">
901
+ <ErrorView error={error}/>
902
+ </div>
903
+ )}
904
+
905
+ <div className="w-full mb-3">
906
+ <Typography variant="label" color="secondary" className="mb-1">
907
+ Email
908
+ </Typography>
909
+ <TextField
910
+ placeholder="you@example.com"
911
+ className={loginFieldClasses}
912
+ autoFocus
913
+ value={email}
914
+ type="email"
915
+ size="medium"
916
+ onChange={(event) => setEmail(event.target.value)}
917
+ />
918
+ </div>
919
+
920
+ <LoadingButton
921
+ type="submit"
922
+ variant="filled"
923
+ className="w-full"
924
+ size="large"
925
+ loading={authController.authLoading}
926
+ disabled={authController.authLoading || !email}
927
+ >
928
+ Send reset link
929
+ </LoadingButton>
930
+ </form>
931
+ );
932
+ }