@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,399 @@
1
+
2
+ import React, { useCallback, useMemo, useRef, useState } from "react";
3
+ import {
4
+ CircleUserIcon,
5
+ CircularProgress,
6
+ cls,
7
+ defaultBorderMixin,
8
+ IconButton,
9
+ iconSize,
10
+ Popover,
11
+ SearchBar,
12
+ SearchIcon,
13
+ Typography,
14
+ XIcon
15
+ } from "@rebasepro/ui";
16
+ import { User } from "@rebasepro/types";
17
+
18
+ /**
19
+ * Represents a selectable user item with optional role information.
20
+ */
21
+ export interface SelectableUser {
22
+ uid: string;
23
+ displayName?: string | null;
24
+ email?: string | null;
25
+ photoURL?: string | null;
26
+ roles?: string[];
27
+ }
28
+
29
+ export interface UserSelectPopoverProps {
30
+ /**
31
+ * The currently selected user, or `null` for "self"/default.
32
+ */
33
+ selectedUser: SelectableUser | null;
34
+ /**
35
+ * Called when the user selection changes.
36
+ * `null` means "reset to self/default".
37
+ */
38
+ onUserSelected: (user: SelectableUser | null) => void;
39
+ /**
40
+ * Full list of users to display.
41
+ * The component handles client-side search/filter internally.
42
+ */
43
+ users: SelectableUser[];
44
+ /**
45
+ * Whether users are currently loading.
46
+ */
47
+ loading?: boolean;
48
+ /**
49
+ * The current user (displayed as the "self" option at the top).
50
+ */
51
+ currentUser?: SelectableUser | null;
52
+ /**
53
+ * Label to display when no user is selected.
54
+ * @default "Current user"
55
+ */
56
+ defaultLabel?: string;
57
+ /**
58
+ * Maximum number of users to render at a time (for performance).
59
+ * Users can still be found via search.
60
+ * @default 100
61
+ */
62
+ renderLimit?: number;
63
+ /**
64
+ * Additional class name for the trigger button.
65
+ */
66
+ className?: string;
67
+ /**
68
+ * Size variant.
69
+ * @default "small"
70
+ */
71
+ size?: "small" | "medium";
72
+ }
73
+
74
+ const ITEM_HEIGHT = 44;
75
+ const MAX_VISIBLE_ITEMS = 7;
76
+
77
+ /**
78
+ * A reusable user picker popover with search support.
79
+ * Designed to handle very large user lists (100k+) by filtering
80
+ * client-side and limiting the rendered set.
81
+ *
82
+ * @group Components
83
+ */
84
+ export function UserSelectPopover({
85
+ selectedUser,
86
+ onUserSelected,
87
+ users,
88
+ loading = false,
89
+ currentUser,
90
+ defaultLabel = "Current user",
91
+ renderLimit = 100,
92
+ className,
93
+ size = "small"
94
+ }: UserSelectPopoverProps) {
95
+ const [open, setOpen] = useState(false);
96
+ const [searchText, setSearchText] = useState("");
97
+ const inputRef = useRef<HTMLInputElement>(null);
98
+ const listRef = useRef<HTMLDivElement>(null);
99
+
100
+ // Effective user label for the trigger
101
+ const effectiveUserLabel = selectedUser
102
+ ? (selectedUser.displayName || selectedUser.email || selectedUser.uid)
103
+ : (currentUser?.displayName || currentUser?.email || defaultLabel);
104
+
105
+ // Filter users based on search text
106
+ const filteredUsers = useMemo(() => {
107
+ const query = searchText.trim().toLowerCase();
108
+ const base = users.filter(u => u.uid !== currentUser?.uid);
109
+
110
+ if (!query) return base.slice(0, renderLimit);
111
+
112
+ const matched = base.filter(u => {
113
+ const name = (u.displayName || "").toLowerCase();
114
+ const email = (u.email || "").toLowerCase();
115
+ const uid = u.uid.toLowerCase();
116
+ const roles = (u.roles || []).join(" ").toLowerCase();
117
+ return (
118
+ name.includes(query) ||
119
+ email.includes(query) ||
120
+ uid.includes(query) ||
121
+ roles.includes(query)
122
+ );
123
+ });
124
+
125
+ return matched.slice(0, renderLimit);
126
+ }, [users, currentUser?.uid, searchText, renderLimit]);
127
+
128
+ const totalFilterable = useMemo(() => {
129
+ const query = searchText.trim().toLowerCase();
130
+ const base = users.filter(u => u.uid !== currentUser?.uid);
131
+ if (!query) return base.length;
132
+ return base.filter(u => {
133
+ const name = (u.displayName || "").toLowerCase();
134
+ const email = (u.email || "").toLowerCase();
135
+ const uid = u.uid.toLowerCase();
136
+ const roles = (u.roles || []).join(" ").toLowerCase();
137
+ return name.includes(query) || email.includes(query) || uid.includes(query) || roles.includes(query);
138
+ }).length;
139
+ }, [users, currentUser?.uid, searchText]);
140
+
141
+ const handleSelect = useCallback((user: SelectableUser | null) => {
142
+ onUserSelected(user);
143
+ setOpen(false);
144
+ setSearchText("");
145
+ }, [onUserSelected]);
146
+
147
+ const handleOpenChange = useCallback((isOpen: boolean) => {
148
+ setOpen(isOpen);
149
+ if (isOpen) {
150
+ // Auto-focus the search input when opening
151
+ setTimeout(() => inputRef.current?.focus(), 50);
152
+ } else {
153
+ setSearchText("");
154
+ }
155
+ }, []);
156
+
157
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
158
+ if (e.key === "Escape") {
159
+ setOpen(false);
160
+ setSearchText("");
161
+ }
162
+ }, []);
163
+
164
+ const listHeight = Math.min(
165
+ (filteredUsers.length + 1) * ITEM_HEIGHT, // +1 for the "self" option
166
+ MAX_VISIBLE_ITEMS * ITEM_HEIGHT
167
+ );
168
+
169
+ return (
170
+ <Popover
171
+ open={open}
172
+ onOpenChange={handleOpenChange}
173
+ side="bottom"
174
+ align="end"
175
+ sideOffset={4}
176
+ trigger={
177
+ <button
178
+ className={cls(
179
+ "flex items-center gap-1.5 text-xs font-medium transition-colors rounded border border-transparent",
180
+ "text-text-secondary dark:text-text-secondary-dark",
181
+ "hover:text-text-primary dark:hover:text-text-primary-dark",
182
+ "bg-surface-100 hover:bg-surface-200 dark:bg-surface-950 dark:hover:bg-surface-700",
183
+ size === "small" ? "px-2 py-1" : "px-3 py-1.5",
184
+ className
185
+ )}
186
+ >
187
+ {selectedUser?.photoURL ? (
188
+ <img
189
+ src={selectedUser.photoURL}
190
+ alt=""
191
+ className="w-4 h-4 rounded-full object-cover"
192
+ />
193
+ ) : (
194
+ <svg
195
+ className="w-3.5 h-3.5 text-text-disabled dark:text-text-disabled-dark"
196
+ fill="none"
197
+ stroke="currentColor"
198
+ viewBox="0 0 24 24"
199
+ >
200
+ <path
201
+ strokeLinecap="round"
202
+ strokeLinejoin="round"
203
+ strokeWidth={2}
204
+ d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
205
+ />
206
+ </svg>
207
+ )}
208
+ <span className="max-w-[120px] truncate">{effectiveUserLabel}</span>
209
+ {selectedUser && (
210
+ <span className="text-[9px] text-amber-600 dark:text-amber-400 font-bold uppercase">
211
+ impersonating
212
+ </span>
213
+ )}
214
+ <svg className="w-3 h-3 opacity-50 ml-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
215
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7"/>
216
+ </svg>
217
+ </button>
218
+ }
219
+ >
220
+ <div
221
+ className="flex flex-col w-[320px] max-w-[90vw] overflow-hidden"
222
+ onKeyDown={handleKeyDown}
223
+ >
224
+ {/* Header */}
225
+ <div className={cls("px-3 py-2 border-b flex items-center justify-between shrink-0", defaultBorderMixin)}>
226
+ <Typography
227
+ variant="caption"
228
+ className="font-bold uppercase tracking-wider text-[9px] text-text-disabled dark:text-text-disabled-dark"
229
+ >
230
+ Run as user
231
+ </Typography>
232
+ {users.length > 0 && (
233
+ <Typography
234
+ variant="caption"
235
+ className="text-[9px] text-text-disabled dark:text-text-disabled-dark tabular-nums"
236
+ >
237
+ {totalFilterable} user{totalFilterable !== 1 ? "s" : ""}
238
+ </Typography>
239
+ )}
240
+ </div>
241
+
242
+ {/* Search input */}
243
+ <div className={cls("px-2 py-1.5 border-b shrink-0", defaultBorderMixin)}>
244
+ <SearchBar
245
+ inputRef={inputRef}
246
+ size="smallest"
247
+ placeholder="Search by name, email, or role…"
248
+ onTextSearch={(val) => setSearchText(val ?? "")}
249
+ />
250
+ </div>
251
+
252
+ {/* User list */}
253
+ <div
254
+ ref={listRef}
255
+ className="overflow-y-auto overscroll-contain"
256
+ style={{ maxHeight: MAX_VISIBLE_ITEMS * ITEM_HEIGHT }}
257
+ >
258
+ {/* Self (default) option — always shown unless search hides it */}
259
+ {currentUser && (!searchText.trim() || matchesSearch(currentUser, searchText)) && (
260
+ <UserRow
261
+ user={currentUser}
262
+ isSelected={!selectedUser}
263
+ isSelf
264
+ onClick={() => handleSelect(null)}
265
+ />
266
+ )}
267
+
268
+ {/* Loading state */}
269
+ {loading && (
270
+ <div className="flex items-center justify-center py-6">
271
+ <CircularProgress size="small"/>
272
+ </div>
273
+ )}
274
+
275
+ {/* Empty state */}
276
+ {!loading && filteredUsers.length === 0 && searchText.trim() && (
277
+ <div className="flex flex-col items-center justify-center py-6 px-4">
278
+ <Typography variant="caption" className="text-text-disabled dark:text-text-disabled-dark text-center">
279
+ No users matching &ldquo;{searchText}&rdquo;
280
+ </Typography>
281
+ </div>
282
+ )}
283
+
284
+ {/* User items */}
285
+ {!loading && filteredUsers.map(user => (
286
+ <UserRow
287
+ key={user.uid}
288
+ user={user}
289
+ isSelected={selectedUser?.uid === user.uid}
290
+ onClick={() => handleSelect(user)}
291
+ />
292
+ ))}
293
+
294
+ {/* Truncation notice */}
295
+ {!loading && totalFilterable > renderLimit && (
296
+ <div className={cls("px-3 py-2 border-t text-center", defaultBorderMixin)}>
297
+ <Typography
298
+ variant="caption"
299
+ className="text-[10px] text-text-disabled dark:text-text-disabled-dark"
300
+ >
301
+ Showing {renderLimit} of {totalFilterable} results. Use search to narrow down.
302
+ </Typography>
303
+ </div>
304
+ )}
305
+ </div>
306
+ </div>
307
+ </Popover>
308
+ );
309
+ }
310
+
311
+ // ─── Internal Components ─────────────────────────────────────────────
312
+
313
+ interface UserRowProps {
314
+ user: SelectableUser;
315
+ isSelected: boolean;
316
+ isSelf?: boolean;
317
+ onClick: () => void;
318
+ }
319
+
320
+ function UserRow({ user, isSelected, isSelf, onClick }: UserRowProps) {
321
+ return (
322
+ <button
323
+ onClick={onClick}
324
+ className={cls(
325
+ "w-full flex items-center gap-2.5 px-3 py-2 text-left transition-colors",
326
+ "hover:bg-surface-accent-100 dark:hover:bg-surface-accent-900",
327
+ isSelected && "bg-primary/5 dark:bg-primary-dark/10"
328
+ )}
329
+ style={{ minHeight: ITEM_HEIGHT }}
330
+ >
331
+ {/* Avatar */}
332
+ {user.photoURL ? (
333
+ <img
334
+ src={user.photoURL}
335
+ alt=""
336
+ className="w-7 h-7 rounded-full object-cover shrink-0"
337
+ />
338
+ ) : (
339
+ <div className="w-7 h-7 rounded-full bg-surface-200 dark:bg-surface-700 flex items-center justify-center shrink-0">
340
+ <CircleUserIcon
341
+ size={"smallest"}
342
+ className="text-text-disabled dark:text-text-disabled-dark"
343
+ />
344
+ </div>
345
+ )}
346
+
347
+ {/* User info */}
348
+ <div className="flex flex-col min-w-0 flex-grow">
349
+ <div className="flex items-center gap-1.5">
350
+ <span
351
+ className={cls(
352
+ "text-xs font-medium truncate",
353
+ isSelected
354
+ ? "text-primary dark:text-primary-dark"
355
+ : "text-text-primary dark:text-text-primary-dark"
356
+ )}
357
+ >
358
+ {user.displayName || user.email || user.uid}
359
+ </span>
360
+ {isSelf && (
361
+ <span className="text-[9px] text-text-disabled dark:text-text-disabled-dark font-medium shrink-0">
362
+ (you)
363
+ </span>
364
+ )}
365
+ </div>
366
+ {(user.email || user.roles?.length) && (
367
+ <span className="text-[10px] text-text-disabled dark:text-text-disabled-dark truncate">
368
+ {user.email && user.displayName ? user.email : ""}
369
+ {user.email && user.displayName && user.roles?.length ? " · " : ""}
370
+ {user.roles?.length ? user.roles.join(", ") : ""}
371
+ </span>
372
+ )}
373
+ </div>
374
+
375
+ {/* Selection indicator */}
376
+ {isSelected && (
377
+ <svg
378
+ className="w-4 h-4 text-primary dark:text-primary-dark shrink-0"
379
+ fill="none"
380
+ stroke="currentColor"
381
+ viewBox="0 0 24 24"
382
+ >
383
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7"/>
384
+ </svg>
385
+ )}
386
+ </button>
387
+ );
388
+ }
389
+
390
+ // ─── Helpers ─────────────────────────────────────────────────────────
391
+
392
+ function matchesSearch(user: SelectableUser, searchText: string): boolean {
393
+ const query = searchText.trim().toLowerCase();
394
+ if (!query) return true;
395
+ const name = (user.displayName || "").toLowerCase();
396
+ const email = (user.email || "").toLowerCase();
397
+ const uid = user.uid.toLowerCase();
398
+ return name.includes(query) || email.includes(query) || uid.includes(query);
399
+ }