@graph8/sdk 0.362.0 → 0.373.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 (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,264 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SuppressionsScreen: the finished do-not-contact page. A filterable ledger with a detail panel for the contact behind
5
+ * a row, a "Suppress contacts" sheet (paste ids or tick people from the CRM) and a confirmed Reinstate.
6
+ *
7
+ * Composes: ScreenLayout, StatStrip, Toolbar, SplitView, DetailPanel, FormSheet (screen kit), ContactTable and
8
+ * ContactDetail (crm), ConfirmSpendDialog.
9
+ *
10
+ * User flows:
11
+ * - filter the ledger by channel and reason, page through it, see how many are suppressed on each channel
12
+ * - Suppress contacts: search and tick people (ContactPicker), or pick them from the CRM table, or paste ids under
13
+ * "Paste ids instead"; choose the channel (default all) and a reason
14
+ * - open a ledger row: the contact's record plus its live suppression status (read-only check)
15
+ * - check any contact without opening it (pick them by name; no ids to type)
16
+ * - tick rows and Reinstate (confirmed: it lets sequences, campaigns and newsletters contact them again)
17
+ *
18
+ * SDK operations, in order:
19
+ * 1. listSuppressions GET /contacts/suppressions?channel&category&limit&offset (read)
20
+ * 2. bulkAddSuppressions POST /contacts/suppressions/bulk-add { contact_ids, channel, reason } (write; only reduces outreach, no confirm)
21
+ * 3. checkContactSuppression GET /contacts/{contact_id}/suppression (read)
22
+ * 4. reinstateSuppressions POST /contacts/suppressions/reinstate { contact_ids, reason } (write, ConfirmSpendDialog "external")
23
+ * 5. listContacts / getContact via ContactTable / ContactDetail (read)
24
+ */
25
+
26
+ import { useEffect, useState } from "react";
27
+ import type { ReactNode } from "react";
28
+ import { BadgeCheck, Ban, ShieldOff, Undo2 } from "lucide-react";
29
+ import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
30
+ import { ApiError } from "@/g8/components/api-error";
31
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
32
+ import { EmptyState } from "@/g8/components/empty-state";
33
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
34
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
35
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
36
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
37
+ import { SectionCard } from "@/g8/components/screen/section-card";
38
+ import { SplitView } from "@/g8/components/screen/split-view";
39
+ import { StatusPill } from "@/g8/components/screen/status-pill";
40
+ import { Toolbar } from "@/g8/components/screen/toolbar";
41
+ import { ContactPicker } from "@/g8/pickers/components/contact-picker";
42
+ import type { ContactOption } from "@/g8/pickers/components/contact-picker";
43
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
44
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
45
+ import { useUrlState } from "@/g8/hooks/use-url-state";
46
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
47
+ import { ContactDetail } from "@/g8/crm/components/contact-detail";
48
+ import { ContactTable } from "@/g8/crm/components/contact-table";
49
+ import { plural } from "@/g8/lib/plural";
50
+ import { parseIds } from "./suppression-manager";
51
+ import type { SuppressionRecord } from "./suppression-manager";
52
+
53
+ export interface SuppressionsScreenProps {
54
+ pageSize?: number;
55
+ toolbarActions?: ReactNode;
56
+ renderRowActions?: (row: SuppressionRecord) => ReactNode;
57
+ renderDetailActions?: (contactId: number) => ReactNode;
58
+ extraDetailTabs?: { id: string; label: string; render: (contactId: number) => ReactNode }[];
59
+ className?: string;
60
+ }
61
+
62
+ interface ContactStatus { contact_id: number; is_suppressed: boolean; active_channels: string[]; suppressions: SuppressionRecord[] }
63
+ interface BulkAdd { suppressed: number; total_requested: number; channel: string; not_found: number[] }
64
+ interface Reinstate { reinstated: number[]; skipped: Array<{ contact_id: number; reason: string }>; not_found: number[] }
65
+
66
+ const CHANNELS = [["all", "All channels"], ["email", "Email"], ["phone", "Phone"], ["linkedin", "LinkedIn"]] as const;
67
+ const SELECT = "h-9 rounded-md border bg-background px-2 text-sm";
68
+
69
+ function ContactStatusPanel({ contactId }: { contactId: number }) {
70
+ const status = useG8Query((signal) => g8Get<ContactStatus>(`/contacts/${contactId}/suppression`, undefined, { signal }), [contactId]);
71
+ if (status.error) return <ApiError error={status.error} title="Could not check that contact" onRetry={() => void status.refetch()} />;
72
+ if (!status.data) return <Skeleton className="h-6 w-48" />;
73
+ const s = status.data.data;
74
+ return (
75
+ <p role="status" className="flex items-center gap-2 text-sm">
76
+ {s.is_suppressed ? <StatusPill tone="negative" dot>Suppressed</StatusPill> : <StatusPill tone="positive" dot>Can be contacted</StatusPill>}
77
+ {s.is_suppressed ? <span className="text-muted-foreground">on {s.active_channels.join(", ")}</span> : null}
78
+ </p>
79
+ );
80
+ }
81
+
82
+ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, className }: SuppressionsScreenProps) {
83
+ const [channel, setChannel] = useState("");
84
+ const [category, setCategory] = useState("");
85
+ const [offset, setOffset] = useState(0);
86
+ const [openId, setOpenId] = useUrlState("id");
87
+ const [view, setView] = useUrlState("view");
88
+ const full = view === "full";
89
+ const [adding, setAdding] = useState(false);
90
+ const [idsText, setIdsText] = useState("");
91
+ const [pickedContacts, setPickedContacts] = useState<ContactOption[]>([]);
92
+ const [addChannel, setAddChannel] = useState("all");
93
+ const [reason, setReason] = useState("");
94
+ const [picked, setPicked] = useState<Set<number>>(new Set());
95
+ const [confirmReinstate, setConfirmReinstate] = useState(false);
96
+ /** Set when Reinstate is started from a row's panel: the confirm acts on that contact only and leaves the ticked rows alone. */
97
+ const [panelTarget, setPanelTarget] = useState<number | null>(null);
98
+ const [reinstateReason, setReinstateReason] = useState("");
99
+ const [checked, setChecked] = useState<ContactOption | null>(null);
100
+
101
+ const ledger = useG8Query((signal) => g8Get<SuppressionRecord[]>("/contacts/suppressions", { channel: channel || undefined, category: category || undefined, limit: pageSize, offset }, { signal }), [channel, category, offset, pageSize]);
102
+ const rows = (ledger.data?.data ?? []) as SuppressionRecord[];
103
+ // Selection belongs to the rows on screen: prune it when the page or a filter changes.
104
+ useEffect(() => { setPicked((p) => new Set([...p].filter((id) => rows.some((r) => r.contact_id === id)))); }, [ledger.data]); // eslint-disable-line react-hooks/exhaustive-deps
105
+
106
+ const add = useG8Mutation((v: { ids: number[] }) => g8Post<BulkAdd>("/contacts/suppressions/bulk-add", { contact_ids: v.ids, channel: addChannel, reason: reason.trim() || undefined }), {
107
+ onSuccess: () => { setIdsText(""); setPickedContacts([]); setReason(""); void ledger.refetch(); },
108
+ });
109
+ const reinstate = useG8Mutation((ids: number[]) => g8Post<Reinstate>("/contacts/suppressions/reinstate", { contact_ids: ids, reason: reinstateReason.trim() || undefined }), {
110
+ onSuccess: () => { setConfirmReinstate(false); setReinstateReason(""); if (panelTarget != null) { const done = panelTarget; setPicked((p) => { const n = new Set(p); n.delete(done); return n; }); } else setPicked(new Set()); setPanelTarget(null); void ledger.refetch(); },
111
+ });
112
+
113
+ const ids = [...new Set([...pickedContacts.map((c) => c.id), ...parseIds(idsText)])];
114
+ const chosen = panelTarget != null ? [panelTarget] : [...picked];
115
+ const togglePick = (id: number) => setPicked((p) => { const n = new Set(p); if (!n.delete(id)) n.add(id); return n; });
116
+ const byChannel = (c: string) => rows.filter((r) => r.channel === c).length;
117
+ const resetPage = () => { setOffset(0); setPicked(new Set()); };
118
+
119
+ const list = (
120
+ <div className="flex flex-col gap-3">
121
+ {ledger.error ? <ApiError error={ledger.error} title="Could not load the ledger" onRetry={() => void ledger.refetch()} /> : null}
122
+ {reinstate.error ? <ApiError error={reinstate.error} title="Could not reinstate" /> : null}
123
+ {reinstate.data ? <p role="status" className="text-sm">Reinstated {reinstate.data.data.reinstated.length}.{reinstate.data.data.skipped.length ? ` Skipped ${reinstate.data.data.skipped.length}: ${reinstate.data.data.skipped.map((s) => `${s.contact_id} (${s.reason})`).join(", ")}.` : ""}</p> : null}
124
+ <div className="divide-y rounded-md border">
125
+ {ledger.loading && rows.length === 0
126
+ ? Array.from({ length: 5 }, (_, i) => <div key={`sk${i}`} className="p-3"><Skeleton className="h-6 w-full" /></div>)
127
+ : rows.map((r) => (
128
+ <div key={r.id} data-testid="suppression-row" className={`flex items-center gap-3 p-3 ${String(r.contact_id) === openId ? "bg-muted/50" : ""}`}>
129
+ <input type="checkbox" aria-label={`Select contact ${r.contact_id}`} checked={picked.has(r.contact_id)} onChange={() => togglePick(r.contact_id)} />
130
+ <button type="button" className="flex min-w-0 flex-1 items-center gap-3 text-left" onClick={() => setOpenId(String(r.contact_id))}>
131
+ <span className="text-sm font-medium tabular-nums">#{r.contact_id}</span>
132
+ <StatusPill tone="negative">{r.channel === "all" ? "All channels" : r.channel}</StatusPill>
133
+ <span className="min-w-0 flex-1 truncate text-sm text-muted-foreground">{r.category.replace(/_/g, " ")}{r.reason ? `: ${r.reason}` : ""}</span>
134
+ <span className="text-xs text-muted-foreground">{r.created_at ? new Date(r.created_at).toLocaleDateString() : ""}</span>
135
+ </button>
136
+ {renderRowActions?.(r)}
137
+ </div>
138
+ ))}
139
+ {!ledger.loading && !ledger.error && rows.length === 0 ? (
140
+ <EmptyState icon={<ShieldOff className="size-6" aria-hidden />} title="Nobody is suppressed" description="Suppressed contacts are skipped by every sequence, campaign and newsletter."
141
+ action={<Button size="sm" onClick={() => setAdding(true)}>Suppress contacts</Button>} />
142
+ ) : null}
143
+ </div>
144
+ <div className="flex items-center justify-end gap-2 text-sm">
145
+ <Button variant="outline" size="sm" disabled={offset === 0 || ledger.loading} onClick={() => { setOffset(Math.max(0, offset - pageSize)); setPicked(new Set()); }}>Previous</Button>
146
+ <Button variant="outline" size="sm" disabled={rows.length < pageSize || ledger.loading} onClick={() => { setOffset(offset + pageSize); setPicked(new Set()); }}>Next</Button>
147
+ </div>
148
+ </div>
149
+ );
150
+
151
+ const contactId = openId ? Number(openId) : null;
152
+ const detail = contactId ? (
153
+ <DetailPanel
154
+ key={contactId}
155
+ onExpand={full ? undefined : () => setView("full")}
156
+ onCollapse={full ? () => setView(null) : undefined}
157
+ title={`Contact #${contactId}`}
158
+ subtitle="Suppression status and record"
159
+ onClose={() => setOpenId(null)}
160
+ actions={(<>
161
+ <Button size="sm" variant="outline" onClick={() => { setPanelTarget(contactId); setConfirmReinstate(true); }}><Undo2 className="mr-1 size-4" aria-hidden />Reinstate</Button>
162
+ {renderDetailActions?.(contactId)}
163
+ </>)}
164
+ tabs={[
165
+ { id: "status", label: "Status", content: (
166
+ <div className="flex flex-col gap-3">
167
+ <ContactStatusPanel contactId={contactId} />
168
+ {rows.filter((r) => r.contact_id === contactId).map((r) => (
169
+ <SectionCard key={r.id} title={r.channel === "all" ? "All channels" : `${r.channel} suppression`} description={r.category.replace(/_/g, " ")}>
170
+ <KeyValueList columns={2} items={[
171
+ { label: "Reason", value: r.reason ?? "", wide: true },
172
+ { label: "Source", value: r.source ?? "" },
173
+ { label: "Source reference", value: r.source_ref ?? "", mono: true },
174
+ { label: "Added by", value: r.created_by ?? "" },
175
+ { label: "Since", value: r.created_at ? new Date(r.created_at).toLocaleString() : "" },
176
+ { label: "Revision", value: r.revision != null ? String(r.revision) : "" },
177
+ { label: "Entry id", value: r.id, mono: true },
178
+ ]} />
179
+ </SectionCard>
180
+ ))}
181
+ </div>
182
+ ) },
183
+ { id: "contact", label: "Contact", content: <ContactDetail contactId={contactId} /> },
184
+ ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(contactId) })),
185
+ ]}
186
+ />
187
+ ) : null;
188
+
189
+ return (
190
+ <ScreenLayout
191
+ className={className}
192
+ title="Suppressions"
193
+ description="Everyone on this list is skipped by every sequence, campaign and newsletter."
194
+ icon={<Ban className="size-5" aria-hidden />}
195
+ actions={<>{toolbarActions}<Button size="sm" onClick={() => setAdding(true)}><Ban className="mr-1 size-4" aria-hidden />Suppress contacts</Button></>}
196
+ stats={[
197
+ { label: "On this page", value: rows.length.toLocaleString() },
198
+ { label: "All channels", value: byChannel("all") },
199
+ { label: "Email", value: byChannel("email") },
200
+ { label: "Phone and LinkedIn", value: byChannel("phone") + byChannel("linkedin") },
201
+ ]}
202
+ toolbar={(
203
+ <Toolbar
204
+ filters={(<>
205
+ <select aria-label="Filter by channel" className={SELECT} value={channel} onChange={(e) => { setChannel(e.target.value); resetPage(); }}>
206
+ <option value="">Any channel</option><option value="all">All</option><option value="email">Email</option><option value="phone">Phone</option><option value="linkedin">LinkedIn</option>
207
+ </select>
208
+ <select aria-label="Filter by reason" className={SELECT} value={category} onChange={(e) => { setCategory(e.target.value); resetPage(); }}>
209
+ <option value="">Any reason</option><option value="contact_initiated">Contact asked</option><option value="org_initiated">Our team</option><option value="system">System</option>
210
+ </select>
211
+ <ContactPicker label="Contact to check" placeholder="Check a contact" value={checked} onChange={setChecked} />
212
+ {checked ? <span className="flex items-center gap-1"><ContactStatusPanel key={checked.id} contactId={checked.id} /><span className="text-xs text-muted-foreground">{checked.name}</span></span> : null}
213
+ </>)}
214
+ actions={<Button variant="outline" size="sm" disabled={chosen.length === 0} onClick={() => { setPanelTarget(null); setConfirmReinstate(true); }}><BadgeCheck className="mr-1 size-4" aria-hidden />Reinstate {chosen.length || ""}</Button>}
215
+ />
216
+ )}
217
+ >
218
+ <SplitView expanded={full} onExpandedChange={(e) => setView(e ? "full" : null)} list={list} detail={detail} detailOpen={contactId != null} onCloseDetail={() => setOpenId(null)} />
219
+
220
+ <FormSheet open={adding} onOpenChange={setAdding} title="Suppress contacts" description="They will be skipped by every sequence, campaign and newsletter on the chosen channels.">
221
+ <div className="flex flex-col gap-4">
222
+ <div className="flex flex-wrap items-end gap-2">
223
+ <div className="flex flex-col gap-1"><Label htmlFor="suppress-channel">Channel</Label>
224
+ <select id="suppress-channel" className={SELECT} value={addChannel} onChange={(e) => setAddChannel(e.target.value)}>{CHANNELS.map(([v, l]) => <option key={v} value={v}>{l}</option>)}</select>
225
+ </div>
226
+ <div className="flex min-w-40 flex-1 flex-col gap-1"><Label htmlFor="suppress-reason">Reason (optional)</Label><Input id="suppress-reason" value={reason} onChange={(e) => setReason(e.target.value)} /></div>
227
+ </div>
228
+ <div className="flex flex-col gap-2">
229
+ <span className="text-sm font-medium leading-none">Search and add people</span>
230
+ <ContactPicker multiple label="Contacts to suppress" selected={pickedContacts} onSelectedChange={setPickedContacts} />
231
+ <details className="text-sm">
232
+ <summary className="cursor-pointer text-muted-foreground">Paste ids instead</summary>
233
+ <div className="mt-1 flex flex-col gap-1">
234
+ <Label htmlFor="suppress-ids">Contact ids</Label>
235
+ <Textarea id="suppress-ids" rows={3} placeholder="101, 102, 103" value={idsText} onChange={(e) => setIdsText(e.target.value)} />
236
+ </div>
237
+ </details>
238
+ <Button size="sm" loading={add.loading} disabled={ids.length === 0} onClick={() => void add.mutate({ ids })}>{ids.length ? `Suppress ${plural(ids.length, "contact")}` : "Suppress contacts"}</Button>
239
+ </div>
240
+ <div className="flex flex-col gap-2">
241
+ <Label>Or pick people from your CRM</Label>
242
+ <ContactTable selectable allowDelete={false} pageSize={8}
243
+ renderBulkActions={({ ids: sel, clear }) => (
244
+ <Button size="sm" variant="outline" loading={add.loading} onClick={async () => { if (await add.mutate({ ids: sel })) clear(); }}>Suppress {sel.length} selected</Button>
245
+ )} />
246
+ </div>
247
+ {add.data ? <p role="status" className="text-sm">Suppressed {add.data.data.suppressed} of {add.data.data.total_requested}.{add.data.data.not_found.length ? ` ${add.data.data.not_found.length} ids do not exist.` : ""}</p> : null}
248
+ {add.error ? <ApiError error={add.error} title="Could not suppress" /> : null}
249
+ </div>
250
+ </FormSheet>
251
+
252
+ <ConfirmSpendDialog open={confirmReinstate} tier="external" title={`Reinstate ${plural(chosen.length, "contact")}`}
253
+ description={(
254
+ <div className="flex flex-col gap-2">
255
+ <p>Reinstating a contact lifts EVERY suppression on it (all channels), not only the row you ticked. They can be messaged again by sequences, campaigns and newsletters. Contacts who unsubscribed themselves may be refused.</p>
256
+ <Label htmlFor="reinstate-reason">Reason (kept in the audit trail)</Label>
257
+ <Input id="reinstate-reason" value={reinstateReason} onChange={(e) => setReinstateReason(e.target.value)} />
258
+ </div>
259
+ )}
260
+ recipients={chosen.length} confirmLabel="Reinstate" confirming={reinstate.loading}
261
+ onConfirm={() => void reinstate.mutate(chosen)} onCancel={() => { setConfirmReinstate(false); setPanelTarget(null); }} />
262
+ </ScreenLayout>
263
+ );
264
+ }
@@ -5,6 +5,23 @@ and check deliverability and warmup before a send. The Apollo / Outreach "mailbo
5
5
 
6
6
  Depends on: `foundation`. Install: `npx @graph8/sdk add mailboxes`.
7
7
 
8
+ ## Screens
9
+
10
+ `MailboxesScreen` is the finished page: mount it as the route. Every record type has a complete detail view,
11
+ deep-linkable with `?id=` (plus `?tab=` for the section and `?dtab=` for the detail tab).
12
+
13
+ - **Mailboxes tab**: fleet stat strip and the mailbox list (show archived). `MailboxDetail` opens beside it: every
14
+ field, its domain, warmup analytics, start/stop warmup, placement test (confirmed), test connection, archive,
15
+ restore and delete (confirmed).
16
+ - **Domains tab**: your domains plus `DomainPurchase` (suggest, check, buy). `DomainDetail` (`?tab=domains&id=`):
17
+ every field, the mailboxes on it, connect and check DNS (confirmed), buy mailboxes on it.
18
+ - **Add mailbox** (header button): IMAP/SMTP connect, the Google/Microsoft link-out to the graph8 app (OAuth cannot
19
+ run through the API), and buy-mailboxes on a connected domain.
20
+ - **Deliverability tab**: safe-to-send verdict, fleet counts, degraded mailboxes.
21
+
22
+ Slot props: `toolbarActions`, `renderDetailActions(mailbox)`, `extraDetailTabs`, `appUrl`. `WarmupHealth` gained an
23
+ optional `showFleet` prop and `ConnectMailbox` optional `initialMode` and `domainId`.
24
+
8
25
  ## Components
9
26
 
10
27
  | Component | What it does | Tiers |
@@ -13,26 +13,28 @@
13
13
  "foundation"
14
14
  ],
15
15
  "operations": [
16
- "list_mailboxes_mailboxes_get",
17
- "test_mailbox_connection_mailboxes__mailbox_id__test_connection_post",
18
16
  "archive_mailbox_mailboxes__mailbox_id__archive_post",
19
- "delete_mailbox_mailboxes__mailbox_id__delete",
20
- "create_mailbox_mailboxes_post",
21
- "list_provider_domains_provider_domains_get",
22
- "get_sending_pricing_provider_domains_pricing_get",
23
- "checkout_purchase_mailboxes_provider_domains__domain_id__checkout_mailboxes_post",
24
- "generate_domain_names_provider_domains_generate_domains_post",
25
17
  "check_domain_availability_provider_domains_check_availability_post",
18
+ "check_domain_propagation_provider_domains__domain_id__check_propagation_post",
26
19
  "checkout_purchase_domains_provider_domains_checkout_purchase_post",
20
+ "checkout_purchase_mailboxes_provider_domains__domain_id__checkout_mailboxes_post",
27
21
  "connect_provider_provider_domains__domain_id__connect_provider_post",
28
- "check_domain_propagation_provider_domains__domain_id__check_propagation_post",
22
+ "create_mailbox_mailboxes_post",
23
+ "delete_mailbox_mailboxes__mailbox_id__delete",
24
+ "disable_warmup_warmup__mailbox_id__disable_post",
25
+ "enable_warmup_warmup__mailbox_id__enable_post",
26
+ "generate_domain_names_provider_domains_generate_domains_post",
29
27
  "get_fleet_summary_deliverability_fleet_summary_get",
28
+ "get_mailbox_mailboxes__mailbox_id__get",
29
+ "get_mailbox_warmup_analytics_mailboxes__mailbox_id__warmup_get",
30
30
  "get_safe_to_send_deliverability_safe_to_send_get",
31
+ "get_sending_pricing_provider_domains_pricing_get",
31
32
  "list_degraded_mailboxes_deliverability_degraded_get",
32
- "get_mailbox_warmup_analytics_mailboxes__mailbox_id__warmup_get",
33
- "enable_warmup_warmup__mailbox_id__enable_post",
34
- "disable_warmup_warmup__mailbox_id__disable_post",
35
- "run_mailbox_placement_test_mailboxes__mailbox_id__health_placement_test_post"
33
+ "list_mailboxes_mailboxes_get",
34
+ "list_provider_domains_provider_domains_get",
35
+ "run_mailbox_placement_test_mailboxes__mailbox_id__health_placement_test_post",
36
+ "test_mailbox_connection_mailboxes__mailbox_id__test_connection_post",
37
+ "update_mailbox_mailboxes__mailbox_id__patch"
36
38
  ],
37
39
  "components": [
38
40
  {
@@ -56,11 +58,12 @@
56
58
  "file": "components/connect-mailbox.tsx",
57
59
  "purpose": "Connect over IMAP/SMTP and test, link to the app for Google/Microsoft, or buy mailboxes on a connected domain (confirmed).",
58
60
  "operations": [
61
+ "checkout_purchase_mailboxes_provider_domains__domain_id__checkout_mailboxes_post",
59
62
  "create_mailbox_mailboxes_post",
60
- "test_mailbox_connection_mailboxes__mailbox_id__test_connection_post",
61
- "list_provider_domains_provider_domains_get",
62
63
  "get_sending_pricing_provider_domains_pricing_get",
63
- "checkout_purchase_mailboxes_provider_domains__domain_id__checkout_mailboxes_post"
64
+ "list_provider_domains_provider_domains_get",
65
+ "test_mailbox_connection_mailboxes__mailbox_id__test_connection_post",
66
+ "update_mailbox_mailboxes__mailbox_id__patch"
64
67
  ],
65
68
  "tiers": [
66
69
  "read",
@@ -103,6 +106,50 @@
103
106
  "write",
104
107
  "billable"
105
108
  ]
109
+ },
110
+ {
111
+ "name": "MailboxesScreen",
112
+ "file": "components/mailboxes-screen.tsx",
113
+ "purpose": "Finished mailboxes page: fleet stats, mailbox list with warmup detail panel, add-mailbox sheet, deliverability and domains tabs.",
114
+ "operations": [
115
+ "get_fleet_summary_deliverability_fleet_summary_get",
116
+ "list_provider_domains_provider_domains_get"
117
+ ],
118
+ "tiers": [
119
+ "read"
120
+ ]
121
+ },
122
+ {
123
+ "name": "MailboxDetail",
124
+ "file": "components/mailbox-detail.tsx",
125
+ "purpose": "One mailbox, complete: every field, its domain, warmup and health; test, archive, restore and delete (confirmed).",
126
+ "operations": [
127
+ "get_mailbox_mailboxes__mailbox_id__get",
128
+ "list_provider_domains_provider_domains_get",
129
+ "test_mailbox_connection_mailboxes__mailbox_id__test_connection_post",
130
+ "archive_mailbox_mailboxes__mailbox_id__archive_post",
131
+ "delete_mailbox_mailboxes__mailbox_id__delete"
132
+ ],
133
+ "tiers": [
134
+ "read",
135
+ "write",
136
+ "destructive"
137
+ ]
138
+ },
139
+ {
140
+ "name": "DomainDetail",
141
+ "file": "components/domain-detail.tsx",
142
+ "purpose": "One sending domain, complete: every field, the mailboxes on it; connect and check DNS (confirmed), buy mailboxes.",
143
+ "operations": [
144
+ "list_provider_domains_provider_domains_get",
145
+ "list_mailboxes_mailboxes_get",
146
+ "connect_provider_provider_domains__domain_id__connect_provider_post",
147
+ "check_domain_propagation_provider_domains__domain_id__check_propagation_post"
148
+ ],
149
+ "tiers": [
150
+ "read",
151
+ "billable"
152
+ ]
106
153
  }
107
154
  ],
108
155
  "hooks": [],
@@ -110,6 +157,10 @@
110
157
  {
111
158
  "file": "lib/mailboxes-utils.ts",
112
159
  "purpose": "Mailbox and domain row types, credit estimate constants, status colors and idempotency-key helper."
160
+ },
161
+ {
162
+ "file": "lib/use-provider-domains.ts",
163
+ "purpose": "Every sending domain, read page after page (the domains API pages) instead of one capped call."
113
164
  }
114
165
  ],
115
166
  "recipes": [
@@ -182,6 +233,7 @@
182
233
  }
183
234
  ],
184
235
  "peerDependencies": {
185
- "react": ">=18"
236
+ "react": ">=18",
237
+ "lucide-react": ">=0.400.0"
186
238
  }
187
239
  }
@@ -24,8 +24,11 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
24
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
25
  import { ApiError } from "@/g8/components/api-error";
26
26
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
- import { DEFAULT_MAILBOX_CREDITS, GRAPH8_APP_MAILBOXES_URL, listOf, newIdempotencyKey, objectOf } from "../lib/mailboxes-utils";
27
+ import { DEFAULT_MAILBOX_CREDITS, GRAPH8_APP_MAILBOXES_PATH, listOf, newIdempotencyKey, objectOf } from "../lib/mailboxes-utils";
28
+ import { useProviderDomains } from "../lib/use-provider-domains";
28
29
  import type { DomainRow } from "../lib/mailboxes-utils";
30
+ import { plural, pluralWord } from "@/g8/lib/plural";
31
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
29
32
 
30
33
  export interface ConnectMailboxProps {
31
34
  /** Where the person connects Google or Microsoft. Default: graph8 Settings > Purchase > Mailboxes. */
@@ -34,6 +37,10 @@ export interface ConnectMailboxProps {
34
37
  onConnected?: (mailboxId: number | string) => void;
35
38
  /** Called with the domain id after mailboxes were bought on it. */
36
39
  onPurchased?: (domainId: number | string) => void;
40
+ /** Open on this tab (default "imap"). */
41
+ initialMode?: "imap" | "oauth" | "buy";
42
+ /** Preselect this connected domain on the Buy tab. */
43
+ domainId?: number | string;
37
44
  className?: string;
38
45
  }
39
46
 
@@ -54,6 +61,7 @@ const EMPTY = {
54
61
  };
55
62
 
56
63
  function ImapForm({ onConnected }: { onConnected?: (id: number | string) => void }) {
64
+ const brand = useBrand();
57
65
  const [form, setForm] = useState(EMPTY);
58
66
  const [result, setResult] = useState<string | null>(null);
59
67
  // Step 1 (create) must not repeat when step 2 (test) fails, even if the person then fixes a password.
@@ -73,7 +81,7 @@ function ImapForm({ onConnected }: { onConnected?: (id: number | string) => void
73
81
  const known = created.current?.email === form.email.trim().toLowerCase() ? created.current : null;
74
82
  if (!known) {
75
83
  const res = await g8Post<{ id: number }>("/mailboxes", body);
76
- if (res.data?.id == null) throw new Error("graph8 did not return the new mailbox id");
84
+ if (res.data?.id == null) throw new Error(`${brand.linkLabel} did not return the new mailbox id`);
77
85
  created.current = { email: form.email.trim().toLowerCase(), id: res.data.id, sig };
78
86
  } else if (known.sig !== sig) {
79
87
  const { email: _email, ...changes } = body;
@@ -128,7 +136,7 @@ function ImapForm({ onConnected }: { onConnected?: (id: number | string) => void
128
136
  </Button>
129
137
  {connect.error ? <ApiError error={connect.error} onRetry={connect.reset} /> : null}
130
138
  {result ? (
131
- <p role="status" className="text-sm text-gray-700">
139
+ <p role="status" className="text-sm text-foreground">
132
140
  {result}
133
141
  </p>
134
142
  ) : null}
@@ -136,10 +144,11 @@ function ImapForm({ onConnected }: { onConnected?: (id: number | string) => void
136
144
  );
137
145
  }
138
146
 
139
- function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void }) {
140
- const domains = useG8Query((signal) => g8Get("/provider-domains", { page: 1, limit: 100 }, { signal }), []);
147
+ function BuyForm({ onPurchased, domainId: initialDomainId }: { onPurchased?: (id: number | string) => void; domainId?: number | string }) {
148
+ const brand = useBrand();
149
+ const domains = useProviderDomains([]);
141
150
  const pricing = useG8Query((signal) => g8Get("/provider-domains/pricing", undefined, { signal }), []);
142
- const [domainId, setDomainId] = useState("");
151
+ const [domainId, setDomainId] = useState(initialDomainId != null ? String(initialDomainId) : "");
143
152
  const [usernames, setUsernames] = useState("");
144
153
  const [person, setPerson] = useState({ first_name: "", last_name: "" });
145
154
  const [platform, setPlatform] = useState<"GOOGLE" | "MICROSOFT">("GOOGLE");
@@ -159,22 +168,22 @@ function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void
159
168
  const sig = `${domainId}:${JSON.stringify(body)}`;
160
169
  if (key.current?.sig !== sig) key.current = { sig, value: newIdempotencyKey() };
161
170
  const res = objectOf(await g8Post(`/provider-domains/${domainId}/checkout-mailboxes`, body, { idempotencyKey: key.current.value }));
162
- setDone(`Accepted ${res.count ?? names.length} mailbox${names.length === 1 ? "" : "es"}; provisioning continues in the background.`);
171
+ setDone(`Accepted ${plural(Number(res.count ?? names.length), "mailbox", "mailboxes")}; provisioning continues in the background.`);
163
172
  onPurchased?.(domainId);
164
173
  });
165
- const connected = listOf<DomainRow>(domains.data).filter((d) => d.provider_connected);
174
+ const connected = domains.items.filter((d) => d.provider_connected);
166
175
  const ready = domainId && names.length > 0 && person.first_name.trim() && person.last_name.trim();
167
176
 
168
177
  return (
169
178
  <div className="space-y-3">
170
179
  {domains.error ? <ApiError error={domains.error} onRetry={() => void domains.refetch()} /> : null}
171
180
  {!domains.loading && connected.length === 0 ? (
172
- <p className="text-sm text-gray-600">No connected domain yet. Buy and connect one with DomainPurchase first.</p>
181
+ <p className="text-sm text-muted-foreground">No connected domain yet. Buy and connect one with DomainPurchase first.</p>
173
182
  ) : (
174
183
  <div className="grid gap-2 sm:grid-cols-2">
175
184
  <select
176
185
  aria-label="Domain"
177
- className="h-9 rounded-md border border-gray-300 px-2 text-sm"
186
+ className="h-9 rounded-md border border-input px-2 text-sm"
178
187
  value={domainId}
179
188
  onChange={(e) => setDomainId(e.target.value)}
180
189
  >
@@ -187,7 +196,7 @@ function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void
187
196
  </select>
188
197
  <select
189
198
  aria-label="Platform"
190
- className="h-9 rounded-md border border-gray-300 px-2 text-sm"
199
+ className="h-9 rounded-md border border-input px-2 text-sm"
191
200
  value={platform}
192
201
  onChange={(e) => setPlatform(e.target.value as "GOOGLE" | "MICROSOFT")}
193
202
  >
@@ -219,11 +228,11 @@ function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void
219
228
  </div>
220
229
  )}
221
230
  <Button disabled={!ready} onClick={() => setConfirming(true)}>
222
- Buy {names.length || ""} mailbox{names.length === 1 ? "" : "es"}
231
+ Buy {names.length || ""} {pluralWord(names.length, "mailbox", "mailboxes")}
223
232
  </Button>
224
233
  {buy.error ? <ApiError error={buy.error} onRetry={buy.reset} /> : null}
225
234
  {done ? (
226
- <p role="status" className="text-sm text-green-700">
235
+ <p role="status" className="text-sm text-positive-foreground">
227
236
  {done}
228
237
  </p>
229
238
  ) : null}
@@ -231,8 +240,8 @@ function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void
231
240
  <ConfirmSpendDialog
232
241
  open
233
242
  tier="billable"
234
- title={`Buy ${names.length} mailbox${names.length === 1 ? "" : "es"}: ${names.join(", ")}`}
235
- description="Mailboxes are billed monthly while they exist, plus warmup if enabled. This is an estimate: graph8 has no balance read."
243
+ title={`Buy ${plural(names.length, "mailbox", "mailboxes")}: ${names.join(", ")}`}
244
+ description={`Mailboxes are billed monthly while they exist, plus warmup if enabled. This is an estimate: ${brand.linkLabel} has no balance read.`}
236
245
  estimatedCredits={names.length * perMailbox}
237
246
  confirmLabel="Buy mailboxes"
238
247
  confirming={buy.loading}
@@ -244,8 +253,10 @@ function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void
244
253
  );
245
254
  }
246
255
 
247
- export function ConnectMailbox({ appUrl = GRAPH8_APP_MAILBOXES_URL, onConnected, onPurchased, className }: ConnectMailboxProps) {
248
- const [mode, setMode] = useState<Mode>("imap");
256
+ export function ConnectMailbox({ appUrl: appUrlProp, onConnected, onPurchased, initialMode = "imap", domainId, className }: ConnectMailboxProps) {
257
+ const brand = useBrand();
258
+ const appUrl = appUrlProp ?? brandUrl(brand, GRAPH8_APP_MAILBOXES_PATH);
259
+ const [mode, setMode] = useState<Mode>(initialMode);
249
260
  return (
250
261
  <div className={`space-y-4 ${className ?? ""}`}>
251
262
  <div className="inline-flex gap-1" role="group" aria-label="How to add a mailbox">
@@ -266,19 +277,19 @@ export function ConnectMailbox({ appUrl = GRAPH8_APP_MAILBOXES_URL, onConnected,
266
277
  </div>
267
278
  {mode === "imap" ? <ImapForm onConnected={onConnected} /> : null}
268
279
  {mode === "oauth" ? (
269
- <div className="space-y-2 rounded border border-gray-200 p-3 text-sm">
280
+ <div className="space-y-2 rounded border border-border p-3 text-sm">
270
281
  <p>
271
- Google and Microsoft mailboxes connect through a sign-in screen that only runs inside the graph8 app. Connect it there, then
282
+ Google and Microsoft mailboxes connect through a sign-in screen that only runs inside the {brand.linkLabel} app. Connect it there, then
272
283
  come back: it appears in your mailbox list.
273
284
  </p>
274
285
  <Button asChild>
275
286
  <a href={appUrl} target="_blank" rel="noreferrer">
276
- Open graph8 to connect
287
+ Open {brand.linkLabel} to connect
277
288
  </a>
278
289
  </Button>
279
290
  </div>
280
291
  ) : null}
281
- {mode === "buy" ? <BuyForm onPurchased={onPurchased} /> : null}
292
+ {mode === "buy" ? <BuyForm onPurchased={onPurchased} domainId={domainId} /> : null}
282
293
  </div>
283
294
  );
284
295
  }