portal-mojo 0.1.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 (369) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -0
  3. package/docs/DetailView.md +173 -0
  4. package/docs/ModelTable.md +128 -0
  5. package/docs/README.md +98 -0
  6. package/docs/admin-assistant.md +72 -0
  7. package/docs/admin-bouncer.md +120 -0
  8. package/docs/admin-cloudwatch.md +38 -0
  9. package/docs/admin-credentials.md +116 -0
  10. package/docs/admin-dashboard.md +15 -0
  11. package/docs/admin-devices-geoip.md +284 -0
  12. package/docs/admin-dns-certificates.md +122 -0
  13. package/docs/admin-dns-records.md +65 -0
  14. package/docs/admin-dns-registrar.md +33 -0
  15. package/docs/admin-dns.md +80 -0
  16. package/docs/admin-identity-users.md +103 -0
  17. package/docs/admin-incidents.md +71 -0
  18. package/docs/admin-jobs.md +426 -0
  19. package/docs/admin-members.md +112 -0
  20. package/docs/admin-messaging.md +56 -0
  21. package/docs/admin-metrics-explorer.md +63 -0
  22. package/docs/admin-monitoring.md +99 -0
  23. package/docs/admin-network-security.md +393 -0
  24. package/docs/admin-phonehub.md +27 -0
  25. package/docs/admin-push.md +36 -0
  26. package/docs/admin-rules.md +87 -0
  27. package/docs/admin-security-tickets.md +40 -0
  28. package/docs/admin-settings.md +78 -0
  29. package/docs/admin-shortlinks.md +32 -0
  30. package/docs/admin-storage.md +141 -0
  31. package/docs/auth-pages.md +212 -0
  32. package/docs/auth.md +58 -0
  33. package/docs/calendar.md +122 -0
  34. package/docs/charts.md +303 -0
  35. package/docs/client.md +206 -0
  36. package/docs/collection-multiselect.md +117 -0
  37. package/docs/collection-select.md +132 -0
  38. package/docs/combobox.md +105 -0
  39. package/docs/dataview.md +226 -0
  40. package/docs/datepicker.md +151 -0
  41. package/docs/daterange.md +170 -0
  42. package/docs/datetimepicker.md +249 -0
  43. package/docs/defineModel.md +72 -0
  44. package/docs/detail-primitives.md +174 -0
  45. package/docs/feedback.md +85 -0
  46. package/docs/file-upload-ux.md +146 -0
  47. package/docs/forms.md +435 -0
  48. package/docs/grouping-and-fmt.md +98 -0
  49. package/docs/idioms.md +112 -0
  50. package/docs/image-editor.md +101 -0
  51. package/docs/loading.md +242 -0
  52. package/docs/location-address.md +76 -0
  53. package/docs/markdown.md +203 -0
  54. package/docs/menus-and-access.md +89 -0
  55. package/docs/multiselect-dropdown.md +153 -0
  56. package/docs/params.md +73 -0
  57. package/docs/popover.md +130 -0
  58. package/docs/realtime.md +61 -0
  59. package/docs/record-feed.md +238 -0
  60. package/docs/right-panel.md +42 -0
  61. package/docs/tabs-and-form-wizard.md +60 -0
  62. package/docs/taginput.md +120 -0
  63. package/docs/timepicker.md +173 -0
  64. package/docs/timezone-select.md +147 -0
  65. package/docs/worldmap.md +302 -0
  66. package/package.json +55 -0
  67. package/src/admin/assistant/AssistantFeed.tsx +29 -0
  68. package/src/admin/assistant/AssistantPanel.tsx +65 -0
  69. package/src/admin/assistant/api.ts +17 -0
  70. package/src/admin/assistant/data.ts +63 -0
  71. package/src/admin/assistant/index.ts +21 -0
  72. package/src/admin/assistant/launchers.tsx +33 -0
  73. package/src/admin/assistant/pages.tsx +78 -0
  74. package/src/admin/assistant/streaming.ts +238 -0
  75. package/src/admin/assistant/types.ts +30 -0
  76. package/src/admin/bouncer/devices.tsx +230 -0
  77. package/src/admin/bouncer/index.ts +34 -0
  78. package/src/admin/bouncer/models.ts +199 -0
  79. package/src/admin/bouncer/signals.tsx +182 -0
  80. package/src/admin/bouncer/signatures.tsx +220 -0
  81. package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
  82. package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
  83. package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
  84. package/src/admin/cloudwatch/data.ts +163 -0
  85. package/src/admin/cloudwatch/index.ts +16 -0
  86. package/src/admin/credentials/group-api-keys.tsx +433 -0
  87. package/src/admin/credentials/index.ts +4 -0
  88. package/src/admin/credentials/models.ts +244 -0
  89. package/src/admin/credentials/secret-dialog.tsx +77 -0
  90. package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
  91. package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
  92. package/src/admin/dashboard/data.ts +60 -0
  93. package/src/admin/dashboard/index.ts +2 -0
  94. package/src/admin/dns/CertificateDetail.tsx +115 -0
  95. package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
  96. package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
  97. package/src/admin/dns/CertificatesPage.tsx +85 -0
  98. package/src/admin/dns/DnsRecordEditor.tsx +110 -0
  99. package/src/admin/dns/DnsRecordsPage.tsx +36 -0
  100. package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
  101. package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
  102. package/src/admin/dns/DomainDetail.tsx +41 -0
  103. package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
  104. package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
  105. package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
  106. package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
  107. package/src/admin/dns/DomainsPage.tsx +27 -0
  108. package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
  109. package/src/admin/dns/RegistrantContactPage.tsx +42 -0
  110. package/src/admin/dns/api.ts +515 -0
  111. package/src/admin/dns/certificate-data.ts +221 -0
  112. package/src/admin/dns/data.ts +32 -0
  113. package/src/admin/dns/dns-data.ts +266 -0
  114. package/src/admin/dns/dns-integration.ts +24 -0
  115. package/src/admin/dns/domain-links.ts +37 -0
  116. package/src/admin/dns/index.ts +86 -0
  117. package/src/admin/dns/models.ts +418 -0
  118. package/src/admin/dns/purchase-data.ts +67 -0
  119. package/src/admin/dns/registrant-data.ts +52 -0
  120. package/src/admin/identity/index.ts +5 -0
  121. package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
  122. package/src/admin/identity/members/MemberDetail.tsx +226 -0
  123. package/src/admin/identity/members/MembersPage.tsx +144 -0
  124. package/src/admin/identity/members/index.ts +24 -0
  125. package/src/admin/identity/members/member-flows.tsx +132 -0
  126. package/src/admin/identity/members/models.ts +204 -0
  127. package/src/admin/identity/users/UserDetail.tsx +257 -0
  128. package/src/admin/identity/users/UsersPage.tsx +163 -0
  129. package/src/admin/identity/users/index.ts +27 -0
  130. package/src/admin/identity/users/models.ts +232 -0
  131. package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
  132. package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
  133. package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
  134. package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
  135. package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
  136. package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
  137. package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
  138. package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
  139. package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
  140. package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
  141. package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
  142. package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
  143. package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
  144. package/src/admin/identity/users/sections/actions.tsx +752 -0
  145. package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
  146. package/src/admin/identity/users/sections/queries.ts +74 -0
  147. package/src/admin/identity/users/sections/shared.tsx +299 -0
  148. package/src/admin/incidents/EventDetail.tsx +43 -0
  149. package/src/admin/incidents/EventsPage.tsx +52 -0
  150. package/src/admin/incidents/IncidentDetail.tsx +68 -0
  151. package/src/admin/incidents/IncidentsPage.tsx +102 -0
  152. package/src/admin/incidents/forensics.tsx +73 -0
  153. package/src/admin/incidents/index.ts +16 -0
  154. package/src/admin/incidents/models.ts +113 -0
  155. package/src/admin/incidents/sanitize.ts +71 -0
  156. package/src/admin/index.ts +383 -0
  157. package/src/admin/jobs/JobDashboardPage.tsx +111 -0
  158. package/src/admin/jobs/JobDetail.tsx +721 -0
  159. package/src/admin/jobs/JobRunnersPage.tsx +216 -0
  160. package/src/admin/jobs/JobsTablePage.tsx +122 -0
  161. package/src/admin/jobs/RunnerDetail.tsx +576 -0
  162. package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
  163. package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
  164. package/src/admin/jobs/columns.tsx +297 -0
  165. package/src/admin/jobs/control.ts +488 -0
  166. package/src/admin/jobs/index.ts +85 -0
  167. package/src/admin/jobs/models.ts +599 -0
  168. package/src/admin/jobs/queries.ts +272 -0
  169. package/src/admin/jobs/scheduled-task-form.tsx +493 -0
  170. package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
  171. package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
  172. package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
  173. package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
  174. package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
  175. package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
  176. package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
  177. package/src/admin/messaging/MailboxesPage.tsx +15 -0
  178. package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
  179. package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
  180. package/src/admin/messaging/SentMessagesPage.tsx +11 -0
  181. package/src/admin/messaging/api.ts +17 -0
  182. package/src/admin/messaging/data.ts +10 -0
  183. package/src/admin/messaging/index.ts +23 -0
  184. package/src/admin/messaging/models.ts +76 -0
  185. package/src/admin/messaging/push/PushPage.tsx +44 -0
  186. package/src/admin/messaging/push/api.ts +17 -0
  187. package/src/admin/messaging/push/index.ts +9 -0
  188. package/src/admin/messaging/push/models.ts +43 -0
  189. package/src/admin/messaging/sanitize.ts +44 -0
  190. package/src/admin/monitoring/LogInspector.tsx +160 -0
  191. package/src/admin/monitoring/LogsPage.tsx +135 -0
  192. package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
  193. package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
  194. package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
  195. package/src/admin/monitoring/index.ts +55 -0
  196. package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
  197. package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
  198. package/src/admin/monitoring/models.ts +155 -0
  199. package/src/admin/network/BlockedIPsPage.tsx +194 -0
  200. package/src/admin/network/BlockedIpDetail.tsx +262 -0
  201. package/src/admin/network/FirewallLogPage.tsx +115 -0
  202. package/src/admin/network/IPSetDetail.tsx +280 -0
  203. package/src/admin/network/IPSetEditor.tsx +191 -0
  204. package/src/admin/network/IPSetsPage.tsx +177 -0
  205. package/src/admin/network/geofence/BlocksTab.tsx +289 -0
  206. package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
  207. package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
  208. package/src/admin/network/geofence/PostureHeader.tsx +184 -0
  209. package/src/admin/network/geofence/RuleEditor.tsx +176 -0
  210. package/src/admin/network/geofence/RulesTab.tsx +254 -0
  211. package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
  212. package/src/admin/network/geofence/geofence-data.ts +674 -0
  213. package/src/admin/network/index.ts +68 -0
  214. package/src/admin/network/models.ts +607 -0
  215. package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
  216. package/src/admin/phonehub/api.ts +47 -0
  217. package/src/admin/phonehub/data.ts +6 -0
  218. package/src/admin/phonehub/index.ts +10 -0
  219. package/src/admin/phonehub/models.ts +68 -0
  220. package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
  221. package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
  222. package/src/admin/rules/RuleSetsPage.tsx +33 -0
  223. package/src/admin/rules/editors.tsx +54 -0
  224. package/src/admin/rules/handler-dsl.ts +213 -0
  225. package/src/admin/rules/index.ts +13 -0
  226. package/src/admin/rules/models.ts +112 -0
  227. package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
  228. package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
  229. package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
  230. package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
  231. package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
  232. package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
  233. package/src/admin/security/devices/index.ts +40 -0
  234. package/src/admin/security/devices/models.ts +470 -0
  235. package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
  236. package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
  237. package/src/admin/security/geoip/geoip-forms.ts +102 -0
  238. package/src/admin/security/geoip/index.ts +27 -0
  239. package/src/admin/security/geoip/models.ts +397 -0
  240. package/src/admin/security/index.ts +31 -0
  241. package/src/admin/security/models.ts +137 -0
  242. package/src/admin/security/tickets.tsx +562 -0
  243. package/src/admin/security-permissions.ts +5 -0
  244. package/src/admin/settings/SettingDetail.tsx +57 -0
  245. package/src/admin/settings/SettingEditor.tsx +157 -0
  246. package/src/admin/settings/SettingsPage.tsx +72 -0
  247. package/src/admin/settings/index.ts +19 -0
  248. package/src/admin/settings/model.ts +143 -0
  249. package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
  250. package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
  251. package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
  252. package/src/admin/shortlinks/index.ts +18 -0
  253. package/src/admin/shortlinks/models.ts +246 -0
  254. package/src/admin/storage/BackendsPage.tsx +43 -0
  255. package/src/admin/storage/BucketDetail.tsx +77 -0
  256. package/src/admin/storage/BucketsPage.tsx +64 -0
  257. package/src/admin/storage/FileManagerDetail.tsx +36 -0
  258. package/src/admin/storage/FilePreview.tsx +54 -0
  259. package/src/admin/storage/FileUploadSurface.tsx +250 -0
  260. package/src/admin/storage/FileView.tsx +109 -0
  261. package/src/admin/storage/FilesPage.tsx +53 -0
  262. package/src/admin/storage/file-renditions.ts +99 -0
  263. package/src/admin/storage/index.ts +31 -0
  264. package/src/admin/storage/models.ts +585 -0
  265. package/src/admin/storage/storage-dialogs.tsx +80 -0
  266. package/src/charts/CircularProgress.tsx +281 -0
  267. package/src/charts/KPIStrip.tsx +162 -0
  268. package/src/charts/KPITile.tsx +125 -0
  269. package/src/charts/MetricsChart.tsx +459 -0
  270. package/src/charts/MetricsMiniWidget.tsx +588 -0
  271. package/src/charts/MiniChart.tsx +283 -0
  272. package/src/charts/PieChart.tsx +354 -0
  273. package/src/charts/SeriesChart.tsx +378 -0
  274. package/src/charts/chart-dialogs.tsx +149 -0
  275. package/src/charts/exportChart.ts +144 -0
  276. package/src/charts/index.ts +28 -0
  277. package/src/charts/pie-math.ts +127 -0
  278. package/src/charts/stats.ts +265 -0
  279. package/src/charts/worldmap/WorldMap.tsx +649 -0
  280. package/src/charts/worldmap/countryCentroids.ts +304 -0
  281. package/src/charts/worldmap/geo.ts +293 -0
  282. package/src/charts/worldmap/use-world-land.ts +47 -0
  283. package/src/charts/worldmap/world-land.ts +18 -0
  284. package/src/charts/worldmap/worldmap-data.ts +285 -0
  285. package/src/client/auth.ts +737 -0
  286. package/src/client/client.ts +268 -0
  287. package/src/client/duid.ts +31 -0
  288. package/src/client/errors.ts +33 -0
  289. package/src/client/group-context.ts +28 -0
  290. package/src/client/group.tsx +126 -0
  291. package/src/client/hooks.ts +50 -0
  292. package/src/client/index.ts +37 -0
  293. package/src/client/jwt.ts +75 -0
  294. package/src/client/location.ts +285 -0
  295. package/src/client/lookups.ts +41 -0
  296. package/src/client/markdown-fallback.tsx +115 -0
  297. package/src/client/markdown-parse.ts +386 -0
  298. package/src/client/markdown-sanitize.ts +217 -0
  299. package/src/client/markdown.ts +122 -0
  300. package/src/client/me.ts +188 -0
  301. package/src/client/mock.ts +9984 -0
  302. package/src/client/model.ts +227 -0
  303. package/src/client/params.ts +292 -0
  304. package/src/client/realtime-mock.ts +221 -0
  305. package/src/client/realtime.ts +561 -0
  306. package/src/client/record-feed.ts +329 -0
  307. package/src/client/safe-export.ts +71 -0
  308. package/src/client/types.ts +278 -0
  309. package/src/client/upload.ts +421 -0
  310. package/src/ui/AddressField.tsx +273 -0
  311. package/src/ui/AttachmentQueue.tsx +92 -0
  312. package/src/ui/CollectionMultiSelect.tsx +622 -0
  313. package/src/ui/CollectionSelect.tsx +517 -0
  314. package/src/ui/ComboBox.tsx +336 -0
  315. package/src/ui/DetailView.tsx +438 -0
  316. package/src/ui/FileDrop.tsx +194 -0
  317. package/src/ui/FileField.tsx +369 -0
  318. package/src/ui/FilterBar.tsx +269 -0
  319. package/src/ui/FormFields.tsx +77 -0
  320. package/src/ui/FormView.tsx +360 -0
  321. package/src/ui/FormWizard.tsx +336 -0
  322. package/src/ui/GroupSwitcher.tsx +202 -0
  323. package/src/ui/Guarded.tsx +16 -0
  324. package/src/ui/MarkdownView.tsx +89 -0
  325. package/src/ui/ModelTable.tsx +848 -0
  326. package/src/ui/MultiSelectDropdown.tsx +390 -0
  327. package/src/ui/Popover.tsx +204 -0
  328. package/src/ui/RecordFeed.tsx +627 -0
  329. package/src/ui/RequiresGroup.tsx +32 -0
  330. package/src/ui/RightPanel.tsx +107 -0
  331. package/src/ui/SidebarNav.tsx +330 -0
  332. package/src/ui/Tabs.tsx +161 -0
  333. package/src/ui/TagInput.tsx +381 -0
  334. package/src/ui/ThemeProvider.tsx +52 -0
  335. package/src/ui/UploadQueue.tsx +603 -0
  336. package/src/ui/armed-button.tsx +91 -0
  337. package/src/ui/date/Calendar.tsx +498 -0
  338. package/src/ui/date/DatePicker.tsx +289 -0
  339. package/src/ui/date/DateRangePicker.tsx +280 -0
  340. package/src/ui/date/DateTimePicker.tsx +587 -0
  341. package/src/ui/date/PresetRail.tsx +165 -0
  342. package/src/ui/date/TimePicker.tsx +624 -0
  343. package/src/ui/date/TimezoneSelect.tsx +267 -0
  344. package/src/ui/date/fns.ts +679 -0
  345. package/src/ui/detail/DataView.tsx +692 -0
  346. package/src/ui/detail/FlowStrip.tsx +80 -0
  347. package/src/ui/detail/KnownFieldsCard.tsx +121 -0
  348. package/src/ui/detail/MetadataSection.tsx +219 -0
  349. package/src/ui/detail/StackTraceView.tsx +150 -0
  350. package/src/ui/detail/StatusPanel.tsx +73 -0
  351. package/src/ui/detail/Timeline.tsx +52 -0
  352. package/src/ui/detail/index.ts +12 -0
  353. package/src/ui/field-registry.tsx +428 -0
  354. package/src/ui/field-wire.ts +322 -0
  355. package/src/ui/form-autosave.ts +583 -0
  356. package/src/ui/format.ts +444 -0
  357. package/src/ui/grouping.ts +150 -0
  358. package/src/ui/image-editor/ImageEditor.tsx +484 -0
  359. package/src/ui/image-editor/canvas.ts +254 -0
  360. package/src/ui/image-editor/index.ts +3 -0
  361. package/src/ui/image-editor/math.ts +416 -0
  362. package/src/ui/index.ts +58 -0
  363. package/src/ui/loading.tsx +604 -0
  364. package/src/ui/menu-registry.ts +213 -0
  365. package/src/ui/modal.tsx +297 -0
  366. package/src/ui/password.tsx +302 -0
  367. package/src/ui/schema-form-core.tsx +315 -0
  368. package/src/ui/toast.tsx +219 -0
  369. package/src/ui/ui.tsx +45 -0
@@ -0,0 +1,390 @@
1
+ // MultiSelectDropdown — the static-options checkbox dropdown: a trigger that
2
+ // summarizes the picks, a menu that STAYS OPEN while you tick rows, and a
3
+ // Done button that closes it. Ported from web-mojo
4
+ // src/core/forms/inputs/MultiSelectDropdown.js (413 lines: MultiSelectDropdown
5
+ // + its MultiSelectItemsView child). Sibling of CollectionMultiSelect — same
6
+ // selection contract, but over a static `options` array instead of live model
7
+ // data, and folded into a dropdown instead of an always-visible panel.
8
+ //
9
+ // The entire open/close/positioning stack of the source (Bootstrap's
10
+ // `data-bs-toggle="dropdown"` + `data-bs-auto-close="outside"` +
11
+ // `bootstrap.Dropdown.getInstance(...).hide()`) is replaced by the shared
12
+ // <Popover> primitive: no Bootstrap, no jQuery-era instance lookups, and the
13
+ // menu stacks above native-<dialog> modals for free.
14
+ //
15
+ // Fixed here BY CONSTRUCTION (web-mojo bugs — do not reintroduce):
16
+ // - THE BUG: selection had TWO owners. The source tracked picks in a private
17
+ // `item.selected` flag driven only by row-click delegation, then wrote
18
+ // `checkbox.checked` by hand — while the row also contained a real focusable
19
+ // <input type="checkbox"> and a <label for> spanning the row. Any toggle the
20
+ // delegation didn't originate (keyboard Space on the focused checkbox, the
21
+ // label's synthesized click) moved the DOM without moving `item.selected`,
22
+ // or moved it twice. Here there is ONE pipeline: the ROW is the checkbox
23
+ // widget (role="checkbox"), the <input> is display-only
24
+ // (pointer-events:none, readOnly, aria-hidden, tabIndex -1), and its checked
25
+ // state is rendered from the controlled `value`. Space on the focused row
26
+ // and a click anywhere on the row run the identical `toggle()`.
27
+ // - Identity never round-trips through DOM attributes. The source read
28
+ // `element.getAttribute('data-value')` — ALWAYS a string — and pushed that
29
+ // into a value array it compared with `includes()` against raw option
30
+ // values, so numeric option values emitted as strings and never matched
31
+ // again. Toggles close over the item; all comparisons pass one keyOf().
32
+ // - Scalar coercion is consistent and falsy-safe. The source's constructor
33
+ // dropped a non-array `value` outright and `setValue` coerced with a truthy
34
+ // test, so `0` and `''` were silently discarded. Only null/undefined mean
35
+ // "nothing selected" here.
36
+ // - A selected value matching no option falls back to String(value) in the
37
+ // trigger summary WITH a console.warn (unknown-value rule) — the source fell
38
+ // back silently.
39
+ import {
40
+ useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState,
41
+ type KeyboardEvent as ReactKeyboardEvent,
42
+ } from 'react';
43
+ import { Popover, type PopoverPlacement } from './Popover';
44
+
45
+ export type MultiSelectValue = string | number;
46
+
47
+ export interface MultiSelectOption {
48
+ value: MultiSelectValue;
49
+ label?: string;
50
+ /** Legacy label alias the source accepted (`option.text`); `label` wins. */
51
+ text?: string;
52
+ /** Row refuses to toggle — by click AND by keyboard. */
53
+ disabled?: boolean;
54
+ }
55
+
56
+ export interface MultiSelectDropdownProps {
57
+ /** Bare strings normalize to `{ value: s, label: s }`. */
58
+ options: ReadonlyArray<MultiSelectOption | string>;
59
+ /**
60
+ * Selected values — CONTROLLED. A bare scalar coerces to a one-element
61
+ * array; null/undefined mean none. String and number values compare
62
+ * normalized, so mixes work by construction.
63
+ */
64
+ value: ReadonlyArray<MultiSelectValue> | MultiSelectValue | null | undefined;
65
+ /** Every toggle IS a commit — fires with the full next array. */
66
+ onChange: (values: MultiSelectValue[]) => void;
67
+ label?: string;
68
+ required?: boolean;
69
+ help?: string;
70
+ /** Replaces `help` when set. */
71
+ error?: string;
72
+ /** Trigger text while nothing is selected. Default 'Select...'. */
73
+ placeholder?: string;
74
+ /** px before the option list scrolls. Default 300. */
75
+ maxHeight?: number;
76
+ /** Summarize with option labels (else always "N selected"). Default true. */
77
+ showSelectedLabels?: boolean;
78
+ /** Above this many picks the trigger shows "N selected". Default 3. */
79
+ maxLabelsToShow?: number;
80
+ /** Locks the trigger and every row. */
81
+ disabled?: boolean;
82
+ /** Menu placement against the trigger. Default 'bottom-start'. */
83
+ placement?: PopoverPlacement;
84
+ /** id for the trigger button (the label points at it). */
85
+ id?: string;
86
+ className?: string;
87
+ }
88
+
89
+ /** Every value comparison in this component goes through here — string and
90
+ * number values (and mixes of the two) work by construction. */
91
+ const keyOf = (v: MultiSelectValue): string => String(v);
92
+
93
+ interface Item {
94
+ /** Normalized identity — the one string every comparison uses. */
95
+ key: string;
96
+ /** The option's raw value, emitted through onChange when picked. */
97
+ value: MultiSelectValue;
98
+ label: string;
99
+ disabled: boolean;
100
+ }
101
+
102
+ /** null/undefined → none; a bare scalar → one pick (0 and '' included). */
103
+ function toArray(
104
+ value: ReadonlyArray<MultiSelectValue> | MultiSelectValue | null | undefined,
105
+ ): MultiSelectValue[] {
106
+ if (value == null) return [];
107
+ return Array.isArray(value) ? [...value] : [value as MultiSelectValue];
108
+ }
109
+
110
+ export function MultiSelectDropdown({
111
+ options,
112
+ value,
113
+ onChange,
114
+ label,
115
+ required = false,
116
+ help,
117
+ error,
118
+ placeholder = 'Select...',
119
+ maxHeight = 300,
120
+ showSelectedLabels = true,
121
+ maxLabelsToShow = 3,
122
+ disabled = false,
123
+ placement = 'bottom-start',
124
+ id,
125
+ className,
126
+ }: MultiSelectDropdownProps) {
127
+ const triggerRef = useRef<HTMLButtonElement>(null);
128
+ const menuRef = useRef<HTMLDivElement>(null);
129
+ const [open, setOpen] = useState(false);
130
+ // Menu width is measured from the trigger (the source's `w-100` dropdown
131
+ // menu) — set at open time so the popover's FIRST render is already the
132
+ // right width, then kept in sync while it stays open.
133
+ const [menuWidth, setMenuWidth] = useState<number>();
134
+
135
+ const autoId = useId();
136
+ const menuId = `${autoId}-menu`;
137
+ const labelId = `${autoId}-label`;
138
+ const triggerId = id ?? `${autoId}-trigger`;
139
+
140
+ const items = useMemo<Item[]>(() => {
141
+ const out: Item[] = [];
142
+ for (const raw of options) {
143
+ if (typeof raw === 'string') {
144
+ out.push({ key: raw, value: raw, label: raw, disabled: false });
145
+ continue;
146
+ }
147
+ if (raw?.value == null) {
148
+ console.warn('MultiSelectDropdown: option without a `value` — dropped', raw);
149
+ continue;
150
+ }
151
+ out.push({
152
+ key: keyOf(raw.value),
153
+ value: raw.value,
154
+ // `label` wins, `text` is the source's alias, else the value.
155
+ label: raw.label || raw.text || String(raw.value),
156
+ disabled: raw.disabled === true,
157
+ });
158
+ }
159
+ return out;
160
+ }, [options]);
161
+
162
+ const selected = useMemo(() => toArray(value), [value]);
163
+ const selectedKeys = useMemo(() => new Set(selected.map(keyOf)), [selected]);
164
+
165
+ // Trigger summary: placeholder → up to N comma-joined labels → "N selected"
166
+ // (source parity, including the selection-ORDER label list).
167
+ const labelByKey = useMemo(() => {
168
+ const map = new Map<string, string>();
169
+ for (const it of items) map.set(it.key, it.label);
170
+ return map;
171
+ }, [items]);
172
+ const unmatched = useMemo(
173
+ () => selected.filter((v) => !labelByKey.has(keyOf(v))),
174
+ [selected, labelByKey],
175
+ );
176
+ const triggerText = selected.length === 0
177
+ ? placeholder
178
+ : showSelectedLabels && selected.length <= maxLabelsToShow
179
+ // Unknown values fall back to their own String() form (warned below)
180
+ // — never a blank row in the summary.
181
+ ? selected.map((v) => labelByKey.get(keyOf(v)) ?? String(v)).join(', ')
182
+ : `${selected.length} selected`;
183
+
184
+ // Warn once per unknown value, out of render (a value with no option is a
185
+ // config/data mismatch, not a paint event).
186
+ const warned = useRef(new Set<string>());
187
+ useEffect(() => {
188
+ for (const v of unmatched) {
189
+ const k = keyOf(v);
190
+ if (warned.current.has(k)) continue;
191
+ warned.current.add(k);
192
+ console.warn(`MultiSelectDropdown: selected value "${k}" matches no option — showing it raw`);
193
+ }
194
+ }, [unmatched]);
195
+
196
+ const close = useCallback((restoreFocus: boolean) => {
197
+ setOpen(false);
198
+ if (restoreFocus) triggerRef.current?.focus();
199
+ }, []);
200
+
201
+ const openMenu = () => {
202
+ const rect = triggerRef.current?.getBoundingClientRect();
203
+ if (rect) setMenuWidth(rect.width);
204
+ setOpen(true);
205
+ };
206
+
207
+ // Keep the menu glued to the trigger's width while it is open (a resized
208
+ // window or a reflowing form column must not leave a stale width).
209
+ useLayoutEffect(() => {
210
+ if (!open) return;
211
+ const el = triggerRef.current;
212
+ if (!el) return;
213
+ const sync = () => setMenuWidth(el.getBoundingClientRect().width);
214
+ sync();
215
+ const ro = new ResizeObserver(sync);
216
+ ro.observe(el);
217
+ return () => ro.disconnect();
218
+ }, [open]);
219
+
220
+ const rows = () => Array.from(
221
+ menuRef.current?.querySelectorAll<HTMLElement>('[data-ms-row]:not([aria-disabled="true"])') ?? [],
222
+ );
223
+
224
+ // Opening moves focus to the first enabled row: the menu is portaled to
225
+ // the end of <body>, so Tab from the trigger would NOT reach it. Focus
226
+ // returns to the trigger on Escape/Done (see close()). Programmatic focus
227
+ // after a mouse click doesn't paint a ring — the row styles :focus-visible.
228
+ useEffect(() => {
229
+ if (!open) return;
230
+ rows()[0]?.focus();
231
+ }, [open]);
232
+
233
+ const toggle = (item: Item) => {
234
+ if (disabled || item.disabled) return;
235
+ if (selectedKeys.has(item.key)) onChange(selected.filter((v) => keyOf(v) !== item.key));
236
+ else onChange([...selected, item.value]);
237
+ };
238
+
239
+ const moveFocus = (from: HTMLElement, delta: 1 | -1) => {
240
+ const list = rows();
241
+ if (list.length === 0) return;
242
+ const at = list.indexOf(from);
243
+ list[(at + delta + list.length) % list.length]?.focus();
244
+ };
245
+
246
+ const onRowKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>, item: Item) => {
247
+ switch (event.key) {
248
+ case ' ':
249
+ case 'Enter':
250
+ // The SAME toggle() a click runs — the desync fix is that there
251
+ // is nothing else to run.
252
+ event.preventDefault();
253
+ toggle(item);
254
+ return;
255
+ case 'ArrowDown':
256
+ case 'ArrowUp':
257
+ event.preventDefault();
258
+ moveFocus(event.currentTarget, event.key === 'ArrowDown' ? 1 : -1);
259
+ return;
260
+ case 'Home':
261
+ case 'End': {
262
+ event.preventDefault();
263
+ const list = rows();
264
+ (event.key === 'Home' ? list[0] : list[list.length - 1])?.focus();
265
+ return;
266
+ }
267
+ default:
268
+ }
269
+ };
270
+
271
+ return (
272
+ <div className={`multiselect-dropdown${disabled ? ' is-disabled' : ''}${className ? ` ${className}` : ''}`}>
273
+ {label && (
274
+ <div className="field-label" id={labelId}>
275
+ {label}
276
+ {required && <em> *</em>}
277
+ </div>
278
+ )}
279
+
280
+ <button
281
+ ref={triggerRef}
282
+ type="button"
283
+ id={triggerId}
284
+ className={`multiselect-trigger${open ? ' is-open' : ''}`}
285
+ disabled={disabled}
286
+ aria-haspopup="dialog"
287
+ aria-expanded={open}
288
+ aria-controls={open ? menuId : undefined}
289
+ // BOTH ids: the field label AND the trigger's own summary, so
290
+ // the name reads "Status, New, Open" — aria-labelledby with the
291
+ // label alone would drop the summary from the accessible name.
292
+ aria-labelledby={label ? `${labelId} ${triggerId}` : undefined}
293
+ onClick={() => (open ? close(true) : openMenu())}
294
+ onKeyDown={(event) => {
295
+ if (!open && event.key === 'ArrowDown') {
296
+ event.preventDefault();
297
+ openMenu();
298
+ }
299
+ }}
300
+ >
301
+ <span className={`multiselect-trigger-text${selected.length === 0 ? ' is-placeholder' : ''}`}>
302
+ {triggerText}
303
+ </span>
304
+ <i className="bi bi-chevron-down" aria-hidden="true" />
305
+ </button>
306
+
307
+ <Popover
308
+ anchorRef={triggerRef}
309
+ open={open}
310
+ placement={placement}
311
+ // Escape always hands focus back; an outside click only does
312
+ // when focus is still parked inside the menu (never fight the
313
+ // element the user just clicked).
314
+ onClose={(reason) => close(
315
+ reason === 'escape' || menuRef.current?.contains(document.activeElement) === true,
316
+ )}
317
+ id={menuId}
318
+ aria-label={label ?? 'Options'}
319
+ style={menuWidth === undefined ? undefined : { width: menuWidth }}
320
+ >
321
+ <div className="multiselect-menu" ref={menuRef}>
322
+ {items.length === 0 ? (
323
+ // Source parity: the empty menu has no Done footer.
324
+ <div className="multiselect-empty">No options available</div>
325
+ ) : (
326
+ <>
327
+ <div
328
+ className="multiselect-list"
329
+ style={{ maxHeight }}
330
+ role="group"
331
+ aria-label={label}
332
+ >
333
+ {items.map((item) => {
334
+ const checked = selectedKeys.has(item.key);
335
+ const rowDisabled = disabled || item.disabled;
336
+ return (
337
+ <div
338
+ key={item.key}
339
+ data-ms-row=""
340
+ role="checkbox"
341
+ aria-checked={checked}
342
+ aria-disabled={rowDisabled || undefined}
343
+ tabIndex={rowDisabled ? -1 : 0}
344
+ className={[
345
+ 'multiselect-item',
346
+ checked ? 'is-selected' : '',
347
+ rowDisabled ? 'is-disabled' : '',
348
+ ].filter(Boolean).join(' ')}
349
+ onClick={() => toggle(item)}
350
+ onKeyDown={(event) => onRowKeyDown(event, item)}
351
+ >
352
+ {/* Display-only (pointer-events:none in CSS): every
353
+ interaction lands on the row, so the box can never
354
+ show a state `value` doesn't hold. */}
355
+ <input
356
+ type="checkbox"
357
+ className="tbl-check"
358
+ checked={checked}
359
+ disabled={rowDisabled}
360
+ tabIndex={-1}
361
+ aria-hidden="true"
362
+ readOnly
363
+ />
364
+ <span className="multiselect-item-label">{item.label}</span>
365
+ </div>
366
+ );
367
+ })}
368
+ </div>
369
+ <div className="multiselect-footer">
370
+ <button
371
+ type="button"
372
+ className="btn btn-primary btn-compact multiselect-done"
373
+ onClick={() => close(true)}
374
+ >
375
+ Done
376
+ </button>
377
+ </div>
378
+ </>
379
+ )}
380
+ </div>
381
+ </Popover>
382
+
383
+ {error ? (
384
+ <div className="field-error">{error}</div>
385
+ ) : help ? (
386
+ <div className="field-help">{help}</div>
387
+ ) : null}
388
+ </div>
389
+ );
390
+ }
@@ -0,0 +1,204 @@
1
+ // Popover — the shared anchored-popover primitive every dropdown-style
2
+ // control mounts in (pickers, autocomplete, multi-selects). Ported from
3
+ // web-mojo CalendarPopover.js: portal-mounted, anchor-positioned, closes on
4
+ // outside-mousedown/Escape, repositions on scroll (capture) and resize.
5
+ //
6
+ // The dialog problem, and why this is not just a z-indexed div: portal-mojo
7
+ // modals are native `<dialog showModal>` — members of the browser TOP LAYER,
8
+ // which paints above the entire document regardless of z-index. A
9
+ // body-portaled div therefore renders UNDER an open modal. The popover joins
10
+ // the top layer itself via the HTML Popover API (`popover="manual"` +
11
+ // `showPopover()`): top-layer members paint in promotion order, so a popover
12
+ // opened from inside a modal stacks above it. `manual` (not `auto`) opts out
13
+ // of UA light-dismiss — close behavior stays ours, matching the source.
14
+ // Browsers without the API fall back to a plain portaled div; when the
15
+ // anchor sits inside an open <dialog> the portal target becomes that dialog
16
+ // (a descendant paints with its top-layer host, and position:fixed children
17
+ // are not clipped by ancestor overflow), so pickers-in-modals keep working.
18
+ //
19
+ // Coordinates: the element is position:fixed in BOTH paths, so all math is
20
+ // viewport-relative (a deliberate translation from the source's
21
+ // absolute+scrollX/Y — the top layer does not scroll with the page, so page
22
+ // coordinates would be wrong there). Scroll repositioning keeps it glued to
23
+ // the anchor.
24
+ //
25
+ // Focus: opening never moves focus — `showPopover()` only focuses content
26
+ // flagged autofocus, and we don't. Typing in an anchored input keeps
27
+ // working; per-option focus retention (e.g. preventDefault on option
28
+ // mousedown) belongs to the consuming control.
29
+ import { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type RefObject } from 'react';
30
+ import { createPortal } from 'react-dom';
31
+
32
+ export type PopoverPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
33
+
34
+ /** Why the popover asked to close — Escape lets pickers revert-vs-commit. */
35
+ export type PopoverCloseReason = 'outside' | 'escape';
36
+
37
+ export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
38
+ /** The trigger element the popover positions against (re-read every
39
+ * reposition, so swapping `.current` re-points the popover). */
40
+ anchorRef: RefObject<HTMLElement | null>;
41
+ /** Controlled visibility — the popover never closes itself; it calls
42
+ * `onClose` and the owner flips `open`. */
43
+ open: boolean;
44
+ /** Outside-mousedown or Escape. Clicks on the anchor are deliberately
45
+ * NOT outside (the anchor's own handler owns its toggle semantics). */
46
+ onClose: (reason: PopoverCloseReason) => void;
47
+ /** Default 'bottom-start'. Unknown values warn + fall back. */
48
+ placement?: PopoverPlacement;
49
+ /** px between anchor and popover (default 6). */
50
+ gap?: number;
51
+ }
52
+
53
+ const PLACEMENTS: readonly PopoverPlacement[] = ['bottom-start', 'bottom-end', 'top-start', 'top-end'];
54
+ const VIEWPORT_INSET = 8; // px breathing room at viewport edges (source parity)
55
+
56
+ const SUPPORTS_POPOVER =
57
+ typeof HTMLElement !== 'undefined' && 'showPopover' in HTMLElement.prototype;
58
+
59
+ export function Popover({
60
+ anchorRef,
61
+ open,
62
+ onClose,
63
+ placement = 'bottom-start',
64
+ gap = 6,
65
+ className,
66
+ children,
67
+ ...rest
68
+ }: PopoverProps) {
69
+ const popRef = useRef<HTMLDivElement>(null);
70
+
71
+ // Latest-callback ref so an inline `onClose` doesn't churn the document
72
+ // listeners (and their deferred attach) on every parent render.
73
+ const onCloseRef = useRef(onClose);
74
+ useEffect(() => { onCloseRef.current = onClose; });
75
+
76
+ // ── Show + position ──────────────────────────────────────────────
77
+ // Layout effect: promote to the top layer and position BEFORE paint, so
78
+ // the popover never flashes at a stale spot.
79
+ useLayoutEffect(() => {
80
+ if (!open) return;
81
+ const el = popRef.current;
82
+ if (!el) return;
83
+
84
+ let place = placement;
85
+ if (!PLACEMENTS.includes(place)) {
86
+ console.warn(`Popover: unknown placement "${String(place)}" — falling back to "bottom-start"`);
87
+ place = 'bottom-start';
88
+ }
89
+
90
+ if (SUPPORTS_POPOVER && !el.matches(':popover-open')) el.showPopover();
91
+
92
+ const reposition = () => {
93
+ const anchor = anchorRef.current;
94
+ if (!anchor) return;
95
+ const rect = anchor.getBoundingClientRect();
96
+ const popRect = el.getBoundingClientRect();
97
+
98
+ let top = rect.bottom + gap;
99
+ let left = rect.left;
100
+ if (place === 'bottom-end') {
101
+ left = rect.right - popRect.width;
102
+ } else if (place === 'top-start') {
103
+ top = rect.top - popRect.height - gap;
104
+ } else if (place === 'top-end') {
105
+ top = rect.top - popRect.height - gap;
106
+ left = rect.right - popRect.width;
107
+ }
108
+
109
+ // Keep within the viewport horizontally.
110
+ const vw = document.documentElement.clientWidth || window.innerWidth;
111
+ if (left + popRect.width > vw - VIEWPORT_INSET) left = vw - popRect.width - VIEWPORT_INSET;
112
+ if (left < VIEWPORT_INSET) left = VIEWPORT_INSET;
113
+
114
+ // Flip above when it would overflow the bottom AND there is room
115
+ // above (any placement — the source's exact rule).
116
+ const vh = document.documentElement.clientHeight || window.innerHeight;
117
+ if (top + popRect.height > vh - VIEWPORT_INSET && rect.top - popRect.height - gap > VIEWPORT_INSET) {
118
+ top = rect.top - popRect.height - gap;
119
+ }
120
+
121
+ el.style.top = `${top}px`;
122
+ el.style.left = `${left}px`;
123
+ };
124
+
125
+ reposition();
126
+ // Capture-phase scroll catches ANY scrolling ancestor, not just the
127
+ // window; resize of the popover (async content) or the anchor
128
+ // re-points without waiting for a scroll.
129
+ const onScroll = () => reposition();
130
+ window.addEventListener('scroll', onScroll, true);
131
+ window.addEventListener('resize', onScroll);
132
+ const ro = new ResizeObserver(reposition);
133
+ ro.observe(el);
134
+ if (anchorRef.current) ro.observe(anchorRef.current);
135
+
136
+ return () => {
137
+ window.removeEventListener('scroll', onScroll, true);
138
+ window.removeEventListener('resize', onScroll);
139
+ ro.disconnect();
140
+ if (SUPPORTS_POPOVER && el.matches(':popover-open')) el.hidePopover();
141
+ };
142
+ }, [open, placement, gap, anchorRef]);
143
+
144
+ // ── Outside-mousedown + Escape ───────────────────────────────────
145
+ useEffect(() => {
146
+ if (!open) return;
147
+
148
+ const onDocDown = (event: MouseEvent) => {
149
+ const target = event.target as Node;
150
+ if (popRef.current?.contains(target)) return;
151
+ if (anchorRef.current?.contains(target)) return;
152
+ // No stopPropagation: the click still activates whatever was
153
+ // under it (normal dropdown UX — click a second trigger and it
154
+ // both closes this popover and opens its own).
155
+ onCloseRef.current('outside');
156
+ };
157
+ const onKey = (event: KeyboardEvent) => {
158
+ if (event.key !== 'Escape') return;
159
+ // Capture phase + preventDefault: when the anchor lives inside
160
+ // an open <dialog>, the first Escape must close the popover
161
+ // ONLY — preventing default stops the dialog's cancel/close
162
+ // request. The next Escape then reaches the dialog.
163
+ event.preventDefault();
164
+ event.stopPropagation();
165
+ onCloseRef.current('escape');
166
+ };
167
+
168
+ // Defer attach so the interaction that opened the popover can't
169
+ // immediately close it (source parity).
170
+ const timer = window.setTimeout(() => {
171
+ document.addEventListener('mousedown', onDocDown, true);
172
+ document.addEventListener('keydown', onKey, true);
173
+ }, 0);
174
+ return () => {
175
+ window.clearTimeout(timer);
176
+ document.removeEventListener('mousedown', onDocDown, true);
177
+ document.removeEventListener('keydown', onKey, true);
178
+ };
179
+ }, [open, anchorRef]);
180
+
181
+ if (!open) return null;
182
+
183
+ // Portal target is decided at render. With the Popover API the top layer
184
+ // makes document.body universal; without it, an anchor inside an open
185
+ // modal portals into that <dialog> so the popover paints with its
186
+ // top-layer host. (Ref read during render is idempotent here — the
187
+ // anchor mounts before any user-driven open.)
188
+ const host = SUPPORTS_POPOVER
189
+ ? document.body
190
+ : anchorRef.current?.closest('dialog') ?? document.body;
191
+
192
+ return createPortal(
193
+ <div
194
+ role="dialog"
195
+ {...rest}
196
+ ref={popRef}
197
+ popover={SUPPORTS_POPOVER ? 'manual' : undefined}
198
+ className={`mojo-popover${className ? ` ${className}` : ''}`}
199
+ >
200
+ {children}
201
+ </div>,
202
+ host,
203
+ );
204
+ }