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,80 @@
1
+ // FlowStrip — the horizontal "STEP 1 → STEP 2 → STEP 3" strip for records
2
+ // that describe a process (RuleSet triggering: Match → Bundle → Threshold →
3
+ // Re-trigger; incident triage; job lifecycle).
4
+ // Port of web-mojo src/core/views/data/FlowStrip.js.
5
+ //
6
+ // ┌──────────┬──────────┬───────────┬────────────┐
7
+ // │ STEP 1 │ STEP 2 │ STEP 3 │ STEP 4 │
8
+ // │ Match ✎ │ Bundle │ Threshold │ Re-trigger │
9
+ // │ value │ value │ value │ value │
10
+ // │ hint │ hint │ hint │ hint │
11
+ // └──────────┴──────────┴───────────┴────────────┘
12
+ //
13
+ // Deviations from source: `value`/`hint` were TRUSTED-HTML strings — they are
14
+ // ReactNode slots here (architecture rule 6); the per-step `action`/`actionData`
15
+ // pair that dispatched through the global action pipeline becomes one
16
+ // `onEditStep(step, index)` callback.
17
+ import type { CSSProperties, ReactNode } from 'react';
18
+ import type { Tone } from '../format';
19
+ import { normalizeTone } from './StatusPanel';
20
+
21
+ export interface FlowStep {
22
+ /** Small uppercase eyebrow. Defaults to `STEP <n>`. */
23
+ num?: string;
24
+ title: string;
25
+ /** Primary value line — a slot, so `<code>`/`<strong>` compose safely. */
26
+ value?: ReactNode;
27
+ /** Small descriptor under the value. */
28
+ hint?: ReactNode;
29
+ /** Renders the value in the muted-italic "not configured" style. */
30
+ empty?: boolean;
31
+ /** Colors the step's eyebrow + rail (per-step signal: which stage failed). */
32
+ tone?: Tone;
33
+ /** Opt a step OUT of the edit affordance when `onEditStep` is wired. */
34
+ editable?: boolean;
35
+ /** Tooltip on the edit button; defaults to `Edit <title>`. */
36
+ editTitle?: string;
37
+ }
38
+
39
+ export function FlowStrip({ steps, onEditStep, editIcon = 'bi-pencil' }: {
40
+ steps: FlowStep[];
41
+ /** Wiring this renders a pencil on every step except `editable: false` ones. */
42
+ onEditStep?: (step: FlowStep, index: number) => void;
43
+ editIcon?: string;
44
+ }) {
45
+ if (!steps.length) return null;
46
+ // Column count rides a custom property so the container queries in CSS can
47
+ // reflow it (4 → 2 → 1) off the strip's OWN width, not the viewport — it is
48
+ // routinely nested inside an `lg` modal.
49
+ const style = { '--flow-strip-cols': steps.length } as CSSProperties;
50
+
51
+ return (
52
+ <div className="detail-flow-strip" style={style}>
53
+ {steps.map((step, i) => {
54
+ const tone = normalizeTone(step.tone, 'FlowStrip');
55
+ const canEdit = !!onEditStep && step.editable !== false;
56
+ return (
57
+ <div key={`${step.title}-${i}`} className={`flow-strip-step tone-${tone}`}>
58
+ <div className="flow-strip-num">{step.num ?? `STEP ${i + 1}`}</div>
59
+ <div className="flow-strip-title">
60
+ {step.title}
61
+ {canEdit && (
62
+ <button
63
+ type="button"
64
+ className="btn-icon btn-icon-sm flow-strip-action"
65
+ title={step.editTitle ?? `Edit ${step.title.toLowerCase()}`}
66
+ aria-label={step.editTitle ?? `Edit ${step.title.toLowerCase()}`}
67
+ onClick={() => onEditStep!(step, i)}
68
+ >
69
+ <i className={`bi ${editIcon}`} />
70
+ </button>
71
+ )}
72
+ </div>
73
+ <div className={`flow-strip-value${step.empty ? ' flow-strip-empty' : ''}`}>{step.value}</div>
74
+ {step.hint != null && step.hint !== '' && <div className="flow-strip-hint">{step.hint}</div>}
75
+ </div>
76
+ );
77
+ })}
78
+ </div>
79
+ );
80
+ }
@@ -0,0 +1,121 @@
1
+ // KnownFieldsCard — the "known JSON keys promoted, raw blob below" pattern.
2
+ // Blob-shaped fields (`metadata`, `ip_info`, `device_info`, `payload`) carry a
3
+ // few keys the framework knows about plus an open-ended bag of extras; detail
4
+ // sections want the known ones in a clean label/value grid and the raw JSON
5
+ // accessible but subordinated.
6
+ // Port of web-mojo src/core/views/data/KnownFieldsCard.js.
7
+ //
8
+ // Created by ian@example.com
9
+ // Reasoning brute-force from same /24
10
+ // Last resolved 2026-04-21 11:42
11
+ // ▶ Raw metadata
12
+ //
13
+ // Deviation from source: `formatter` returned TRUSTED HTML — `format` returns
14
+ // ReactNode here (architecture rule 6). The string form still works and now
15
+ // names an `fmt.*` formatter; an unknown name warns and falls back to the plain
16
+ // value rather than rendering nothing (architecture rule 4).
17
+ import type { ReactNode } from 'react';
18
+ import * as fmt from '../format';
19
+
20
+ /** The `fmt.*` formatters a spec may name as a string. */
21
+ const NAMED_FORMATTERS = {
22
+ date: (v: unknown) => fmt.date(v as string | number | null),
23
+ datetime: (v: unknown) => fmt.datetime(v as string | number | null),
24
+ relative: (v: unknown) => fmt.relative(v as string | number | null),
25
+ } satisfies Record<string, (value: unknown) => ReactNode>;
26
+
27
+ export type NamedFormatter = keyof typeof NAMED_FORMATTERS;
28
+
29
+ export interface KnownField {
30
+ /** Key in the blob. Dotted paths traverse nested objects (`os.family`). */
31
+ key: string;
32
+ /** Row label; defaults to the key itself. */
33
+ label?: string;
34
+ /** An `fmt.*` name, or a function returning any node. */
35
+ format?: NamedFormatter | ((value: unknown, key: string, data: Record<string, unknown>) => ReactNode);
36
+ /** Drop the row entirely when the value is missing (default: show "—"). */
37
+ hideEmpty?: boolean;
38
+ }
39
+
40
+ /**
41
+ * Dotted-path lookup. An own property wins over traversal, so a blob that
42
+ * literally holds the key `"os.family"` still resolves (source behavior).
43
+ */
44
+ function lookup(data: Record<string, unknown>, key: string): unknown {
45
+ if (!key) return undefined;
46
+ if (Object.prototype.hasOwnProperty.call(data, key)) return data[key];
47
+ if (!key.includes('.')) return undefined;
48
+ let cursor: unknown = data;
49
+ for (const part of key.split('.')) {
50
+ if (cursor == null || typeof cursor !== 'object') return undefined;
51
+ cursor = (cursor as Record<string, unknown>)[part];
52
+ }
53
+ return cursor;
54
+ }
55
+
56
+ function formatValue(value: unknown, spec: KnownField, data: Record<string, unknown>): ReactNode {
57
+ const { format } = spec;
58
+ if (typeof format === 'function') return format(value, spec.key, data);
59
+ if (typeof format === 'string') {
60
+ const named = NAMED_FORMATTERS[format];
61
+ if (named) return named(value);
62
+ console.warn(`[KnownFieldsCard] unknown format ${JSON.stringify(format)} for key "${spec.key}" — rendering the raw value. Valid: ${Object.keys(NAMED_FORMATTERS).join(', ')}`);
63
+ }
64
+ // Default: scalars as text, objects as compact JSON in a code span.
65
+ if (value !== null && typeof value === 'object') {
66
+ return <code className="dim">{JSON.stringify(value)}</code>;
67
+ }
68
+ return String(value);
69
+ }
70
+
71
+ export function KnownFieldsCard({ data, known = [], showRaw = true, rawLabel = 'Raw JSON', rawCollapsed = true, emptyText = 'No data.' }: {
72
+ data: Record<string, unknown>;
73
+ known?: KnownField[];
74
+ showRaw?: boolean;
75
+ rawLabel?: string;
76
+ /** `<details>` starts closed (default) or open. */
77
+ rawCollapsed?: boolean;
78
+ emptyText?: string;
79
+ }) {
80
+ if (!known.length && Object.keys(data).length === 0) {
81
+ return <div className="dim detail-known-fields-empty">{emptyText}</div>;
82
+ }
83
+
84
+ const rows = known
85
+ .map((spec) => ({ spec, value: lookup(data, spec.key) }))
86
+ .filter(({ spec, value }) => !(spec.hideEmpty && (value == null || value === '')));
87
+
88
+ let json: string;
89
+ try {
90
+ json = JSON.stringify(data, null, 2);
91
+ } catch {
92
+ // Circular blobs reach here — the grid above is still worth rendering.
93
+ json = String(data);
94
+ }
95
+
96
+ return (
97
+ <div className="detail-known-fields-card">
98
+ {rows.length > 0 && (
99
+ <div className="detail-known-fields-grid">
100
+ {rows.map(({ spec, value }) => {
101
+ const missing = value == null || value === '';
102
+ return (
103
+ <div key={spec.key} className="flat-row detail-known-field-row">
104
+ <div className="flat-label">{spec.label ?? spec.key}</div>
105
+ <div className="flat-value">
106
+ {missing ? <span className="dim-italic">—</span> : formatValue(value, spec, data)}
107
+ </div>
108
+ </div>
109
+ );
110
+ })}
111
+ </div>
112
+ )}
113
+ {showRaw && Object.keys(data).length > 0 && (
114
+ <details className="detail-known-fields-raw" open={!rawCollapsed}>
115
+ <summary className="detail-known-fields-raw-summary">{rawLabel}</summary>
116
+ <pre className="detail-known-fields-raw-body">{json}</pre>
117
+ </details>
118
+ )}
119
+ </div>
120
+ );
121
+ }
@@ -0,0 +1,219 @@
1
+ // MetadataSection — view/edit the `metadata` blob on any django-mojo record.
2
+ // Port of web-mojo src/extensions/admin/shared/AdminMetadataSection.js.
3
+ //
4
+ // Metadata [+ Add]
5
+ // ┌──────────────────────────────────────────────────────────┐
6
+ // │ timezone America/New_York ✎ 🗑 │
7
+ // │ signup_source {"campaign":"q3"} (object, read-only)│
8
+ // └──────────────────────────────────────────────────────────┘
9
+ //
10
+ // Deviations from source:
11
+ // · Editing is INLINE, not a modal round-trip. A rejected save must keep the
12
+ // editor state so the operator can retry without retyping — there is no
13
+ // editor state to keep once a modal has closed.
14
+ // · Source read `resp.status === 200` off a never-rejecting `model.save()`
15
+ // and toasted "Failed to save metadata" — the trap architecture rule 3
16
+ // retires. `mojoCall` REJECTS; the rejection surfaces the server's own
17
+ // message in a persistent inline banner (a toast alone is missable).
18
+ // · Controlled on `metadata` (architecture rule 5): the component never
19
+ // mirrors server state. `onSaved` is REQUIRED — it is how the owner
20
+ // (TanStack cache, parent state) learns about the write.
21
+ import { useState } from 'react';
22
+ import { mojoCall } from '../../client/client';
23
+ import { modal } from '../modal';
24
+ import { toast } from '../toast';
25
+
26
+ type Draft =
27
+ | { mode: 'add'; key: string; value: string }
28
+ | { mode: 'edit'; key: string; value: string }
29
+ | null;
30
+
31
+ /** Objects/arrays render read-only — a string editor round-trips them lossily. */
32
+ function isBlob(value: unknown): boolean {
33
+ return value !== null && typeof value === 'object';
34
+ }
35
+
36
+ function display(value: unknown): string {
37
+ return isBlob(value) ? JSON.stringify(value) : String(value);
38
+ }
39
+
40
+ /**
41
+ * Source semantics: a typed value is JSON-parsed when it can be, so `42`,
42
+ * `true` and `{"a":1}` land as their real types and only the rest stay strings.
43
+ */
44
+ function parseValue(raw: string): unknown {
45
+ try {
46
+ return JSON.parse(raw);
47
+ } catch {
48
+ return raw;
49
+ }
50
+ }
51
+
52
+ export function MetadataSection({ endpoint, id, metadata, onSaved, title = 'Metadata' }: {
53
+ /** Collection endpoint — the save posts to `<endpoint>/<id>`. */
54
+ endpoint: string;
55
+ id: number | string;
56
+ metadata: Record<string, unknown>;
57
+ /** Called with the new blob after the server accepted it. */
58
+ onSaved: (next: Record<string, unknown>) => void;
59
+ title?: string;
60
+ }) {
61
+ const [draft, setDraft] = useState<Draft>(null);
62
+ const [pending, setPending] = useState<string | null>(null);
63
+ const [error, setError] = useState<string | null>(null);
64
+
65
+ const keys = Object.keys(metadata).sort();
66
+ const busy = pending !== null;
67
+
68
+ /** The one write path. Resolves true only when the server accepted it. */
69
+ async function commit(next: Record<string, unknown>, token: string, message: string): Promise<boolean> {
70
+ setPending(token);
71
+ setError(null);
72
+ try {
73
+ await mojoCall(`${endpoint}/${id}`, { method: 'POST', body: { metadata: next } });
74
+ onSaved(next);
75
+ toast.success(message);
76
+ return true;
77
+ } catch (err) {
78
+ // Unmissable failure: the banner persists (a toast would not) and
79
+ // the caller's draft below is deliberately left untouched.
80
+ setError(err instanceof Error ? err.message : String(err));
81
+ return false;
82
+ } finally {
83
+ setPending(null);
84
+ }
85
+ }
86
+
87
+ async function saveDraft() {
88
+ if (!draft) return;
89
+ const key = draft.key.trim();
90
+ if (!key) {
91
+ setError('Key is required.');
92
+ return;
93
+ }
94
+ if (draft.mode === 'add' && Object.prototype.hasOwnProperty.call(metadata, key)) {
95
+ setError(`Key "${key}" already exists — edit it instead.`);
96
+ return;
97
+ }
98
+ const next = { ...metadata, [key]: parseValue(draft.value) };
99
+ const ok = await commit(next, draft.mode === 'add' ? 'add' : `edit:${key}`, draft.mode === 'add' ? 'Metadata entry added' : 'Metadata updated');
100
+ if (ok) setDraft(null); // a failed save keeps the editor open, populated
101
+ }
102
+
103
+ async function removeKey(key: string) {
104
+ const confirmed = await modal.confirm({
105
+ title: 'Remove entry',
106
+ message: <>Remove metadata key <b>{key}</b>?</>,
107
+ confirmText: 'Remove',
108
+ danger: true,
109
+ });
110
+ if (!confirmed) return;
111
+ const next = { ...metadata };
112
+ delete next[key];
113
+ await commit(next, `delete:${key}`, 'Metadata entry removed');
114
+ }
115
+
116
+ const renderEditor = (rowKey: string) => (
117
+ <div className="amd-item amd-editor" key={rowKey}>
118
+ {draft?.mode === 'add' ? (
119
+ <input
120
+ className="input input-compact amd-key-input"
121
+ placeholder="key"
122
+ autoFocus
123
+ value={draft.key}
124
+ onChange={(e) => setDraft({ ...draft, key: e.target.value })}
125
+ onKeyDown={(e) => {
126
+ if (e.key === 'Enter') void saveDraft();
127
+ if (e.key === 'Escape') setDraft(null);
128
+ }}
129
+ />
130
+ ) : (
131
+ <div className="amd-key">{draft?.key}</div>
132
+ )}
133
+ <input
134
+ className="input input-compact amd-value-input"
135
+ placeholder='value (JSON parsed when it can be: 42, true, {"a":1})'
136
+ autoFocus={draft?.mode === 'edit'}
137
+ value={draft?.value ?? ''}
138
+ onChange={(e) => draft && setDraft({ ...draft, value: e.target.value })}
139
+ onKeyDown={(e) => {
140
+ if (e.key === 'Enter') void saveDraft();
141
+ if (e.key === 'Escape') setDraft(null);
142
+ }}
143
+ />
144
+ <div className="amd-actions">
145
+ <button type="button" className="btn btn-primary btn-compact" disabled={busy} onClick={() => void saveDraft()}>
146
+ {busy ? <i className="bi bi-arrow-repeat spin" /> : <i className="bi bi-check-lg" />} Save
147
+ </button>
148
+ <button type="button" className="btn btn-compact" disabled={busy} onClick={() => setDraft(null)}>Cancel</button>
149
+ </div>
150
+ </div>
151
+ );
152
+
153
+ return (
154
+ <div className="admin-metadata-section">
155
+ <div className="amd-header">
156
+ <h6 className="amd-title">{title}</h6>
157
+ <button
158
+ type="button"
159
+ className="btn btn-primary btn-compact"
160
+ disabled={busy || draft?.mode === 'add'}
161
+ onClick={() => { setError(null); setDraft({ mode: 'add', key: '', value: '' }); }}
162
+ >
163
+ <i className="bi bi-plus-lg" /> Add
164
+ </button>
165
+ </div>
166
+
167
+ {error && (
168
+ <div className="amd-error" role="alert">
169
+ <i className="bi bi-exclamation-triangle-fill" /> {error}
170
+ </div>
171
+ )}
172
+
173
+ <div className="amd-list">
174
+ {keys.length === 0 && draft?.mode !== 'add' ? (
175
+ <div className="amd-empty">
176
+ <i className="bi bi-braces" />
177
+ No metadata entries
178
+ </div>
179
+ ) : keys.map((key) => (
180
+ draft?.mode === 'edit' && draft.key === key ? (
181
+ renderEditor(key)
182
+ ) : (
183
+ <div key={key} className="amd-item">
184
+ <div className="amd-key">{key}</div>
185
+ <div className="amd-value">
186
+ {isBlob(metadata[key])
187
+ ? <code className="amd-blob" title="Object values are read-only here — remove and re-add to change them">{display(metadata[key])}</code>
188
+ : display(metadata[key])}
189
+ </div>
190
+ <div className="amd-actions">
191
+ <button
192
+ type="button"
193
+ className="btn-icon btn-icon-sm"
194
+ title={isBlob(metadata[key]) ? 'Object values are read-only' : `Edit ${key}`}
195
+ aria-label={`Edit ${key}`}
196
+ disabled={busy || isBlob(metadata[key])}
197
+ onClick={() => { setError(null); setDraft({ mode: 'edit', key, value: display(metadata[key]) }); }}
198
+ >
199
+ <i className="bi bi-pencil" />
200
+ </button>
201
+ <button
202
+ type="button"
203
+ className="btn-icon btn-icon-sm amd-remove"
204
+ title={`Remove ${key}`}
205
+ aria-label={`Remove ${key}`}
206
+ disabled={busy}
207
+ onClick={() => void removeKey(key)}
208
+ >
209
+ {pending === `delete:${key}` ? <i className="bi bi-arrow-repeat spin" /> : <i className="bi bi-trash" />}
210
+ </button>
211
+ </div>
212
+ </div>
213
+ )
214
+ ))}
215
+ {draft?.mode === 'add' && renderEditor('__add__')}
216
+ </div>
217
+ </div>
218
+ );
219
+ }
@@ -0,0 +1,150 @@
1
+ // StackTraceView — colorized stack traces, JavaScript AND Python.
2
+ // Port of web-mojo src/core/views/data/StackTraceView.js.
3
+ //
4
+ // TypeError: Cannot read properties of undefined ← error line
5
+ // at handleSubmit (app/views/JobDetails.js:214:19) ← function + location
6
+ // File "mojo/jobs/runner.py", line 412, in _execute ← python frame
7
+ //
8
+ // Deviations from source:
9
+ // · RE-THEMED. The source shipped a hardcoded light-only <style> block
10
+ // (#f8f9fa surface, #dc3545/#0d6efd/#6610f2 text) that was unreadable in
11
+ // dark mode. This renders semantic classes over theme tokens, correct in
12
+ // BOTH themes.
13
+ // · Long traces COLLAPSE to `collapseAfter` lines behind a toggle instead of
14
+ // only being capped by a scroll box.
15
+ // · The source only colored an exception on line 0, so every Python
16
+ // traceback — which opens with "Traceback (most recent call last):" and
17
+ // puts the exception LAST — rendered its most important line as plain
18
+ // context. An anchored exception-line pattern now catches it anywhere.
19
+ import { useMemo, useState } from 'react';
20
+
21
+ // Ported verbatim from the source, in the source's match order.
22
+ const JS_CALL = /(.+?)\s*\(([^:]+):(\d+):(\d+)\)/; // at fn (file.js:123:45)
23
+ const JS_AT = /^\s*at\s+([^:]+):(\d+):(\d+)/; // at file.js:123:45
24
+ const PY_FRAME = /File\s+"([^"]+)",\s+line\s+(\d+),\s+in\s+(.+)/;
25
+ // Extension: `ValueError: …`, `django.db.utils.IntegrityError: …`, bare
26
+ // `KeyboardInterrupt`. The trailing `(?::|$)` is what keeps ordinary prose
27
+ // ("Error handling request") out of the error style.
28
+ const EXCEPTION = /^[A-Za-z_][\w.]*(?:Error|Exception|Warning|Interrupt|Exit)(?::|$)/;
29
+
30
+ type Line =
31
+ | { kind: 'blank' }
32
+ | { kind: 'error'; text: string }
33
+ | { kind: 'js-call'; indent: string; lead: string; fn: string; file: string; line: string; col: string }
34
+ | { kind: 'js-at'; indent: string; file: string; line: string; col: string }
35
+ | { kind: 'py'; indent: string; file: string; line: string; fn: string }
36
+ | { kind: 'frame'; text: string }
37
+ | { kind: 'context'; text: string };
38
+
39
+ function parseLine(raw: string, index: number): Line {
40
+ if (!raw.trim()) return { kind: 'blank' };
41
+
42
+ // Error message: the source's first-line rule, plus any anchored
43
+ // exception line (where Python puts it).
44
+ const trimmed = raw.trim();
45
+ if ((index === 0 && (raw.includes('Error:') || raw.includes('Exception:'))) || EXCEPTION.test(trimmed)) {
46
+ return { kind: 'error', text: raw };
47
+ }
48
+ // Matched frames are re-composed from their captures, so the original
49
+ // indentation is carried explicitly — it is what keeps a Python frame
50
+ // aligned with the source line under it, terminal-style.
51
+ const indent = raw.match(/^\s*/)![0];
52
+
53
+ const call = raw.match(JS_CALL);
54
+ if (call) {
55
+ // The capture greedily includes the frame keyword ("at handleSubmit").
56
+ // Split it off so only the identifier reads as an identifier.
57
+ const captured = call[1]!.trim();
58
+ const lead = captured.startsWith('at ') ? 'at ' : '';
59
+ return { kind: 'js-call', indent, lead, fn: captured.slice(lead.length), file: call[2]!, line: call[3]!, col: call[4]! };
60
+ }
61
+
62
+ const at = raw.match(JS_AT);
63
+ if (at) return { kind: 'js-at', indent, file: at[1]!, line: at[2]!, col: at[3]! };
64
+
65
+ const py = raw.match(PY_FRAME);
66
+ if (py) return { kind: 'py', indent, file: py[1]!, line: py[2]!, fn: py[3]! };
67
+
68
+ if (trimmed.startsWith('at ')) return { kind: 'frame', text: raw };
69
+ return { kind: 'context', text: raw };
70
+ }
71
+
72
+ /** Non-string traces (an error payload object) are pretty-printed first. */
73
+ function toText(trace: unknown): string {
74
+ if (typeof trace === 'string') return trace;
75
+ if (trace == null) return '';
76
+ try {
77
+ return JSON.stringify(trace, null, 2);
78
+ } catch {
79
+ return String(trace);
80
+ }
81
+ }
82
+
83
+ export function StackTraceView({ trace, collapseAfter = 24, emptyText = 'No stack trace available' }: {
84
+ /** The raw trace; objects are JSON-pretty-printed. */
85
+ trace: unknown;
86
+ /** Lines shown before the "show all" toggle. 0 disables collapsing. */
87
+ collapseAfter?: number;
88
+ emptyText?: string;
89
+ }) {
90
+ const [expanded, setExpanded] = useState(false);
91
+ const text = toText(trace);
92
+ const lines = useMemo(() => text.split('\n').map(parseLine), [text]);
93
+
94
+ if (!text) return <div className="stack-trace-empty dim">{emptyText}</div>;
95
+
96
+ const collapsible = collapseAfter > 0 && lines.length > collapseAfter;
97
+ const shown = collapsible && !expanded ? lines.slice(0, collapseAfter) : lines;
98
+
99
+ return (
100
+ <div className="stack-trace-view">
101
+ <div className="stack-trace-body">
102
+ {shown.map((line, i) => {
103
+ switch (line.kind) {
104
+ case 'blank':
105
+ return <div key={i} className="stack-trace-line">&nbsp;</div>;
106
+ case 'error':
107
+ return <div key={i} className="stack-trace-line stack-trace-error">{line.text}</div>;
108
+ case 'js-call':
109
+ return (
110
+ <div key={i} className="stack-trace-line stack-trace-frame">
111
+ <span className="stack-trace-loc">{line.indent}{line.lead}</span>
112
+ <span className="stack-trace-fn">{line.fn}</span>
113
+ <span className="stack-trace-loc">
114
+ {' ('}{line.file}:<span className="stack-trace-lineno">{line.line}</span>:{line.col}{')'}
115
+ </span>
116
+ </div>
117
+ );
118
+ case 'js-at':
119
+ return (
120
+ <div key={i} className="stack-trace-line stack-trace-frame">
121
+ <span className="stack-trace-loc">
122
+ {line.indent}at {line.file}:<span className="stack-trace-lineno">{line.line}</span>:{line.col}
123
+ </span>
124
+ </div>
125
+ );
126
+ case 'py':
127
+ return (
128
+ <div key={i} className="stack-trace-line stack-trace-frame">
129
+ <span className="stack-trace-loc">
130
+ {line.indent}File "{line.file}", line <span className="stack-trace-lineno">{line.line}</span>, in{' '}
131
+ </span>
132
+ <span className="stack-trace-fn">{line.fn}</span>
133
+ </div>
134
+ );
135
+ case 'frame':
136
+ return <div key={i} className="stack-trace-line stack-trace-frame">{line.text}</div>;
137
+ default:
138
+ return <div key={i} className="stack-trace-line stack-trace-context">{line.text}</div>;
139
+ }
140
+ })}
141
+ </div>
142
+ {collapsible && (
143
+ <button type="button" className="stack-trace-toggle" onClick={() => setExpanded((v) => !v)}>
144
+ <i className={`bi ${expanded ? 'bi-chevron-up' : 'bi-chevron-down'}`} />
145
+ {expanded ? `Show first ${collapseAfter} lines` : `Show all ${lines.length} lines`}
146
+ </button>
147
+ )}
148
+ </div>
149
+ );
150
+ }
@@ -0,0 +1,73 @@
1
+ // StatusPanel — the hero "current state" banner that opens a record's
2
+ // Overview section (incident triage, job lifecycle, runner alive/dead).
3
+ // Port of web-mojo src/core/views/data/StatusPanel.js.
4
+ //
5
+ // ┌───────────────────────────────────────────────────────────┐
6
+ // │ ● STATE [ primary ] [ alt ] │
7
+ // │ Headline line │
8
+ // │ Optional supporting meta line · with <code>fragments</code>
9
+ // └───────────────────────────────────────────────────────────┘
10
+ //
11
+ // Deviation from source (architecture rule 6): `meta` was a TRUSTED-HTML
12
+ // string with a "caller must escape" contract, and `actions` was a
13
+ // {label, icon, variant, action} array dispatched through the global action
14
+ // pipeline. Both are ReactNode slots here — the caller composes `<code>` /
15
+ // `<button onClick>` directly, so the escaping contract cannot be broken.
16
+ import type { ReactNode } from 'react';
17
+ import type { Tone } from '../format';
18
+
19
+ /**
20
+ * The tone vocabulary shared by the detail primitives. web-mojo's set was
21
+ * {default, primary, success, info, warning, danger, secondary}; portal-mojo's
22
+ * `Tone` folds default/secondary into `muted`, so Badge, StatusPanel, FlowStrip
23
+ * and Timeline all speak one vocabulary.
24
+ */
25
+ export const DETAIL_TONES = new Set<string>(['success', 'warning', 'danger', 'info', 'muted', 'primary']);
26
+
27
+ /**
28
+ * Runtime guard for tones that arrive as wire data (`tone: row.status_tone`)
29
+ * rather than literals. Unknown values fall back to `muted` WITH a warn —
30
+ * never to rendering nothing (architecture rule 4).
31
+ */
32
+ export function normalizeTone(tone: Tone | string | null | undefined, where: string): Tone {
33
+ if (tone == null || tone === '') return 'muted';
34
+ if (DETAIL_TONES.has(tone)) return tone as Tone;
35
+ console.warn(`[${where}] unknown tone ${JSON.stringify(tone)} — falling back to "muted". Valid: ${[...DETAIL_TONES].join(', ')}`);
36
+ return 'muted';
37
+ }
38
+
39
+ export interface StatusPanelProps {
40
+ /** Tints the panel surface and colors the state eyebrow + dot. */
41
+ tone?: Tone;
42
+ /** Small uppercase eyebrow beside the dot ("RUNNING", "ACKNOWLEDGED"). */
43
+ state?: string;
44
+ /** The dominant line — what the operator reads first. */
45
+ headline?: ReactNode;
46
+ /** Supporting line; a slot, so `<code>`/`<strong>` compose safely. */
47
+ meta?: ReactNode;
48
+ /** bootstrap-icons name; replaces the tone dot when given. */
49
+ icon?: string;
50
+ /** Buttons row. A slot — the caller owns the handlers. */
51
+ actions?: ReactNode;
52
+ }
53
+
54
+ export function StatusPanel({ tone, state, headline, meta, icon, actions }: StatusPanelProps) {
55
+ const t = normalizeTone(tone, 'StatusPanel');
56
+ return (
57
+ <div className={`detail-status-panel tone-${t}`}>
58
+ <div className="detail-status-headline">
59
+ {state && (
60
+ <div className="detail-status-state">
61
+ {icon
62
+ ? <i className={`bi ${icon} detail-status-icon`} />
63
+ : <span className="detail-status-dot" />}
64
+ {state}
65
+ </div>
66
+ )}
67
+ {headline != null && headline !== '' && <div className="detail-status-line">{headline}</div>}
68
+ {meta != null && meta !== '' && <div className="detail-status-meta">{meta}</div>}
69
+ </div>
70
+ {actions && <div className="detail-status-actions">{actions}</div>}
71
+ </div>
72
+ );
73
+ }