@graph8/sdk 0.361.0 → 0.370.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 (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,255 @@
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: paste ids, or pick contacts from the CRM, choose the channel (default all) and a reason
13
+ * - open a ledger row: the contact's record plus its live suppression status (read-only check)
14
+ * - check any contact id without opening it
15
+ * - tick rows and Reinstate (confirmed: it lets sequences, campaigns and newsletters contact them again)
16
+ *
17
+ * SDK operations, in order:
18
+ * 1. listSuppressions GET /contacts/suppressions?channel&category&limit&offset (read)
19
+ * 2. bulkAddSuppressions POST /contacts/suppressions/bulk-add { contact_ids, channel, reason } (write; only reduces outreach, no confirm)
20
+ * 3. checkContactSuppression GET /contacts/{contact_id}/suppression (read)
21
+ * 4. reinstateSuppressions POST /contacts/suppressions/reinstate { contact_ids, reason } (write, ConfirmSpendDialog "external")
22
+ * 5. listContacts / getContact via ContactTable / ContactDetail (read)
23
+ */
24
+
25
+ import { useEffect, useState } from "react";
26
+ import type { ReactNode } from "react";
27
+ import { BadgeCheck, Ban, Search, ShieldOff, Undo2 } from "lucide-react";
28
+ import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
29
+ import { ApiError } from "@/g8/components/api-error";
30
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
31
+ import { EmptyState } from "@/g8/components/empty-state";
32
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
33
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
34
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
35
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
36
+ import { SectionCard } from "@/g8/components/screen/section-card";
37
+ import { SplitView } from "@/g8/components/screen/split-view";
38
+ import { StatusPill } from "@/g8/components/screen/status-pill";
39
+ import { Toolbar } from "@/g8/components/screen/toolbar";
40
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
41
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
42
+ import { useUrlState } from "@/g8/hooks/use-url-state";
43
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
44
+ import { ContactDetail } from "@/g8/crm/components/contact-detail";
45
+ import { ContactTable } from "@/g8/crm/components/contact-table";
46
+ import { plural } from "@/g8/lib/plural";
47
+ import { parseIds } from "./suppression-manager";
48
+ import type { SuppressionRecord } from "./suppression-manager";
49
+
50
+ export interface SuppressionsScreenProps {
51
+ pageSize?: number;
52
+ toolbarActions?: ReactNode;
53
+ renderRowActions?: (row: SuppressionRecord) => ReactNode;
54
+ renderDetailActions?: (contactId: number) => ReactNode;
55
+ extraDetailTabs?: { id: string; label: string; render: (contactId: number) => ReactNode }[];
56
+ className?: string;
57
+ }
58
+
59
+ interface ContactStatus { contact_id: number; is_suppressed: boolean; active_channels: string[]; suppressions: SuppressionRecord[] }
60
+ interface BulkAdd { suppressed: number; total_requested: number; channel: string; not_found: number[] }
61
+ interface Reinstate { reinstated: number[]; skipped: Array<{ contact_id: number; reason: string }>; not_found: number[] }
62
+
63
+ const CHANNELS = [["all", "All channels"], ["email", "Email"], ["phone", "Phone"], ["linkedin", "LinkedIn"]] as const;
64
+ const SELECT = "h-9 rounded-md border bg-background px-2 text-sm";
65
+
66
+ function ContactStatusPanel({ contactId }: { contactId: number }) {
67
+ const status = useG8Query((signal) => g8Get<ContactStatus>(`/contacts/${contactId}/suppression`, undefined, { signal }), [contactId]);
68
+ if (status.error) return <ApiError error={status.error} title="Could not check that contact" onRetry={() => void status.refetch()} />;
69
+ if (!status.data) return <Skeleton className="h-6 w-48" />;
70
+ const s = status.data.data;
71
+ return (
72
+ <p role="status" className="flex items-center gap-2 text-sm">
73
+ {s.is_suppressed ? <StatusPill tone="negative" dot>Suppressed</StatusPill> : <StatusPill tone="positive" dot>Can be contacted</StatusPill>}
74
+ {s.is_suppressed ? <span className="text-muted-foreground">on {s.active_channels.join(", ")}</span> : null}
75
+ </p>
76
+ );
77
+ }
78
+
79
+ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, className }: SuppressionsScreenProps) {
80
+ const [channel, setChannel] = useState("");
81
+ const [category, setCategory] = useState("");
82
+ const [offset, setOffset] = useState(0);
83
+ const [openId, setOpenId] = useUrlState("id");
84
+ const [view, setView] = useUrlState("view");
85
+ const full = view === "full";
86
+ const [adding, setAdding] = useState(false);
87
+ const [idsText, setIdsText] = useState("");
88
+ const [addChannel, setAddChannel] = useState("all");
89
+ const [reason, setReason] = useState("");
90
+ const [picked, setPicked] = useState<Set<number>>(new Set());
91
+ const [confirmReinstate, setConfirmReinstate] = useState(false);
92
+ /** Set when Reinstate is started from a row's panel: the confirm acts on that contact only and leaves the ticked rows alone. */
93
+ const [panelTarget, setPanelTarget] = useState<number | null>(null);
94
+ const [reinstateReason, setReinstateReason] = useState("");
95
+ const [checkId, setCheckId] = useState("");
96
+ const [checked, setChecked] = useState<number | null>(null);
97
+
98
+ const ledger = useG8Query((signal) => g8Get<SuppressionRecord[]>("/contacts/suppressions", { channel: channel || undefined, category: category || undefined, limit: pageSize, offset }, { signal }), [channel, category, offset, pageSize]);
99
+ const rows = (ledger.data?.data ?? []) as SuppressionRecord[];
100
+ // Selection belongs to the rows on screen: prune it when the page or a filter changes.
101
+ useEffect(() => { setPicked((p) => new Set([...p].filter((id) => rows.some((r) => r.contact_id === id)))); }, [ledger.data]); // eslint-disable-line react-hooks/exhaustive-deps
102
+
103
+ const add = useG8Mutation((v: { ids: number[] }) => g8Post<BulkAdd>("/contacts/suppressions/bulk-add", { contact_ids: v.ids, channel: addChannel, reason: reason.trim() || undefined }), {
104
+ onSuccess: () => { setIdsText(""); setReason(""); void ledger.refetch(); },
105
+ });
106
+ const reinstate = useG8Mutation((ids: number[]) => g8Post<Reinstate>("/contacts/suppressions/reinstate", { contact_ids: ids, reason: reinstateReason.trim() || undefined }), {
107
+ 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(); },
108
+ });
109
+
110
+ const ids = parseIds(idsText);
111
+ const chosen = panelTarget != null ? [panelTarget] : [...picked];
112
+ const togglePick = (id: number) => setPicked((p) => { const n = new Set(p); if (!n.delete(id)) n.add(id); return n; });
113
+ const byChannel = (c: string) => rows.filter((r) => r.channel === c).length;
114
+ const resetPage = () => { setOffset(0); setPicked(new Set()); };
115
+
116
+ const list = (
117
+ <div className="flex flex-col gap-3">
118
+ {ledger.error ? <ApiError error={ledger.error} title="Could not load the ledger" onRetry={() => void ledger.refetch()} /> : null}
119
+ {reinstate.error ? <ApiError error={reinstate.error} title="Could not reinstate" /> : null}
120
+ {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}
121
+ <div className="divide-y rounded-md border">
122
+ {ledger.loading && rows.length === 0
123
+ ? Array.from({ length: 5 }, (_, i) => <div key={`sk${i}`} className="p-3"><Skeleton className="h-6 w-full" /></div>)
124
+ : rows.map((r) => (
125
+ <div key={r.id} data-testid="suppression-row" className={`flex items-center gap-3 p-3 ${String(r.contact_id) === openId ? "bg-muted/50" : ""}`}>
126
+ <input type="checkbox" aria-label={`Select contact ${r.contact_id}`} checked={picked.has(r.contact_id)} onChange={() => togglePick(r.contact_id)} />
127
+ <button type="button" className="flex min-w-0 flex-1 items-center gap-3 text-left" onClick={() => setOpenId(String(r.contact_id))}>
128
+ <span className="text-sm font-medium tabular-nums">#{r.contact_id}</span>
129
+ <StatusPill tone="negative">{r.channel === "all" ? "All channels" : r.channel}</StatusPill>
130
+ <span className="min-w-0 flex-1 truncate text-sm text-muted-foreground">{r.category.replace(/_/g, " ")}{r.reason ? `: ${r.reason}` : ""}</span>
131
+ <span className="text-xs text-muted-foreground">{r.created_at ? new Date(r.created_at).toLocaleDateString() : ""}</span>
132
+ </button>
133
+ {renderRowActions?.(r)}
134
+ </div>
135
+ ))}
136
+ {!ledger.loading && !ledger.error && rows.length === 0 ? (
137
+ <EmptyState icon={<ShieldOff className="size-6" aria-hidden />} title="Nobody is suppressed" description="Suppressed contacts are skipped by every sequence, campaign and newsletter."
138
+ action={<Button size="sm" onClick={() => setAdding(true)}>Suppress contacts</Button>} />
139
+ ) : null}
140
+ </div>
141
+ <div className="flex items-center justify-end gap-2 text-sm">
142
+ <Button variant="outline" size="sm" disabled={offset === 0 || ledger.loading} onClick={() => { setOffset(Math.max(0, offset - pageSize)); setPicked(new Set()); }}>Previous</Button>
143
+ <Button variant="outline" size="sm" disabled={rows.length < pageSize || ledger.loading} onClick={() => { setOffset(offset + pageSize); setPicked(new Set()); }}>Next</Button>
144
+ </div>
145
+ </div>
146
+ );
147
+
148
+ const contactId = openId ? Number(openId) : null;
149
+ const detail = contactId ? (
150
+ <DetailPanel
151
+ key={contactId}
152
+ onExpand={full ? undefined : () => setView("full")}
153
+ onCollapse={full ? () => setView(null) : undefined}
154
+ title={`Contact #${contactId}`}
155
+ subtitle="Suppression status and record"
156
+ onClose={() => setOpenId(null)}
157
+ actions={(<>
158
+ <Button size="sm" variant="outline" onClick={() => { setPanelTarget(contactId); setConfirmReinstate(true); }}><Undo2 className="mr-1 size-4" aria-hidden />Reinstate</Button>
159
+ {renderDetailActions?.(contactId)}
160
+ </>)}
161
+ tabs={[
162
+ { id: "status", label: "Status", content: (
163
+ <div className="flex flex-col gap-3">
164
+ <ContactStatusPanel contactId={contactId} />
165
+ {rows.filter((r) => r.contact_id === contactId).map((r) => (
166
+ <SectionCard key={r.id} title={r.channel === "all" ? "All channels" : `${r.channel} suppression`} description={r.category.replace(/_/g, " ")}>
167
+ <KeyValueList columns={2} items={[
168
+ { label: "Reason", value: r.reason ?? "", wide: true },
169
+ { label: "Source", value: r.source ?? "" },
170
+ { label: "Source reference", value: r.source_ref ?? "", mono: true },
171
+ { label: "Added by", value: r.created_by ?? "" },
172
+ { label: "Since", value: r.created_at ? new Date(r.created_at).toLocaleString() : "" },
173
+ { label: "Revision", value: r.revision != null ? String(r.revision) : "" },
174
+ { label: "Entry id", value: r.id, mono: true },
175
+ ]} />
176
+ </SectionCard>
177
+ ))}
178
+ </div>
179
+ ) },
180
+ { id: "contact", label: "Contact", content: <ContactDetail contactId={contactId} /> },
181
+ ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(contactId) })),
182
+ ]}
183
+ />
184
+ ) : null;
185
+
186
+ return (
187
+ <ScreenLayout
188
+ className={className}
189
+ title="Suppressions"
190
+ description="Everyone on this list is skipped by every sequence, campaign and newsletter."
191
+ icon={<Ban className="size-5" aria-hidden />}
192
+ actions={<>{toolbarActions}<Button size="sm" onClick={() => setAdding(true)}><Ban className="mr-1 size-4" aria-hidden />Suppress contacts</Button></>}
193
+ stats={[
194
+ { label: "On this page", value: rows.length.toLocaleString() },
195
+ { label: "All channels", value: byChannel("all") },
196
+ { label: "Email", value: byChannel("email") },
197
+ { label: "Phone and LinkedIn", value: byChannel("phone") + byChannel("linkedin") },
198
+ ]}
199
+ toolbar={(
200
+ <Toolbar
201
+ filters={(<>
202
+ <select aria-label="Filter by channel" className={SELECT} value={channel} onChange={(e) => { setChannel(e.target.value); resetPage(); }}>
203
+ <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>
204
+ </select>
205
+ <select aria-label="Filter by reason" className={SELECT} value={category} onChange={(e) => { setCategory(e.target.value); resetPage(); }}>
206
+ <option value="">Any reason</option><option value="contact_initiated">Contact asked</option><option value="org_initiated">Our team</option><option value="system">System</option>
207
+ </select>
208
+ <Input aria-label="Check a contact id" inputMode="numeric" placeholder="Check contact id" className="w-40" value={checkId} onChange={(e) => setCheckId(e.target.value)} />
209
+ <Button variant="outline" size="sm" disabled={!Number(checkId)} onClick={() => setChecked(Number(checkId))}><Search className="mr-1 size-4" aria-hidden />Check</Button>
210
+ {checked ? <span className="flex items-center gap-1"><ContactStatusPanel key={checked} contactId={checked} /><span className="text-xs text-muted-foreground">#{checked}</span></span> : null}
211
+ </>)}
212
+ 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>}
213
+ />
214
+ )}
215
+ >
216
+ <SplitView expanded={full} onExpandedChange={(e) => setView(e ? "full" : null)} list={list} detail={detail} detailOpen={contactId != null} onCloseDetail={() => setOpenId(null)} />
217
+
218
+ <FormSheet open={adding} onOpenChange={setAdding} title="Suppress contacts" description="They will be skipped by every sequence, campaign and newsletter on the chosen channels.">
219
+ <div className="flex flex-col gap-4">
220
+ <div className="flex flex-wrap items-end gap-2">
221
+ <div className="flex flex-col gap-1"><Label htmlFor="suppress-channel">Channel</Label>
222
+ <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>
223
+ </div>
224
+ <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>
225
+ </div>
226
+ <div className="flex flex-col gap-2">
227
+ <Label htmlFor="suppress-ids">Contact ids</Label>
228
+ <Textarea id="suppress-ids" rows={3} placeholder="101, 102, 103" value={idsText} onChange={(e) => setIdsText(e.target.value)} />
229
+ <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>
230
+ </div>
231
+ <div className="flex flex-col gap-2">
232
+ <Label>Or pick people from your CRM</Label>
233
+ <ContactTable selectable allowDelete={false} pageSize={8}
234
+ renderBulkActions={({ ids: sel, clear }) => (
235
+ <Button size="sm" variant="outline" loading={add.loading} onClick={async () => { if (await add.mutate({ ids: sel })) clear(); }}>Suppress {sel.length} selected</Button>
236
+ )} />
237
+ </div>
238
+ {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}
239
+ {add.error ? <ApiError error={add.error} title="Could not suppress" /> : null}
240
+ </div>
241
+ </FormSheet>
242
+
243
+ <ConfirmSpendDialog open={confirmReinstate} tier="external" title={`Reinstate ${plural(chosen.length, "contact")}`}
244
+ description={(
245
+ <div className="flex flex-col gap-2">
246
+ <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>
247
+ <Label htmlFor="reinstate-reason">Reason (kept in the audit trail)</Label>
248
+ <Input id="reinstate-reason" value={reinstateReason} onChange={(e) => setReinstateReason(e.target.value)} />
249
+ </div>
250
+ )}
251
+ recipients={chosen.length} confirmLabel="Reinstate" confirming={reinstate.loading}
252
+ onConfirm={() => void reinstate.mutate(chosen)} onCancel={() => { setConfirmReinstate(false); setPanelTarget(null); }} />
253
+ </ScreenLayout>
254
+ );
255
+ }
@@ -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": [],
@@ -182,6 +229,7 @@
182
229
  }
183
230
  ],
184
231
  "peerDependencies": {
185
- "react": ">=18"
232
+ "react": ">=18",
233
+ "lucide-react": ">=0.400.0"
186
234
  }
187
235
  }
@@ -24,8 +24,10 @@ 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
28
  import type { DomainRow } from "../lib/mailboxes-utils";
29
+ import { plural, pluralWord } from "@/g8/lib/plural";
30
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
29
31
 
30
32
  export interface ConnectMailboxProps {
31
33
  /** Where the person connects Google or Microsoft. Default: graph8 Settings > Purchase > Mailboxes. */
@@ -34,6 +36,10 @@ export interface ConnectMailboxProps {
34
36
  onConnected?: (mailboxId: number | string) => void;
35
37
  /** Called with the domain id after mailboxes were bought on it. */
36
38
  onPurchased?: (domainId: number | string) => void;
39
+ /** Open on this tab (default "imap"). */
40
+ initialMode?: "imap" | "oauth" | "buy";
41
+ /** Preselect this connected domain on the Buy tab. */
42
+ domainId?: number | string;
37
43
  className?: string;
38
44
  }
39
45
 
@@ -54,6 +60,7 @@ const EMPTY = {
54
60
  };
55
61
 
56
62
  function ImapForm({ onConnected }: { onConnected?: (id: number | string) => void }) {
63
+ const brand = useBrand();
57
64
  const [form, setForm] = useState(EMPTY);
58
65
  const [result, setResult] = useState<string | null>(null);
59
66
  // Step 1 (create) must not repeat when step 2 (test) fails, even if the person then fixes a password.
@@ -73,7 +80,7 @@ function ImapForm({ onConnected }: { onConnected?: (id: number | string) => void
73
80
  const known = created.current?.email === form.email.trim().toLowerCase() ? created.current : null;
74
81
  if (!known) {
75
82
  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");
83
+ if (res.data?.id == null) throw new Error(`${brand.linkLabel} did not return the new mailbox id`);
77
84
  created.current = { email: form.email.trim().toLowerCase(), id: res.data.id, sig };
78
85
  } else if (known.sig !== sig) {
79
86
  const { email: _email, ...changes } = body;
@@ -136,10 +143,11 @@ function ImapForm({ onConnected }: { onConnected?: (id: number | string) => void
136
143
  );
137
144
  }
138
145
 
139
- function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void }) {
146
+ function BuyForm({ onPurchased, domainId: initialDomainId }: { onPurchased?: (id: number | string) => void; domainId?: number | string }) {
147
+ const brand = useBrand();
140
148
  const domains = useG8Query((signal) => g8Get("/provider-domains", { page: 1, limit: 100 }, { signal }), []);
141
149
  const pricing = useG8Query((signal) => g8Get("/provider-domains/pricing", undefined, { signal }), []);
142
- const [domainId, setDomainId] = useState("");
150
+ const [domainId, setDomainId] = useState(initialDomainId != null ? String(initialDomainId) : "");
143
151
  const [usernames, setUsernames] = useState("");
144
152
  const [person, setPerson] = useState({ first_name: "", last_name: "" });
145
153
  const [platform, setPlatform] = useState<"GOOGLE" | "MICROSOFT">("GOOGLE");
@@ -159,7 +167,7 @@ function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void
159
167
  const sig = `${domainId}:${JSON.stringify(body)}`;
160
168
  if (key.current?.sig !== sig) key.current = { sig, value: newIdempotencyKey() };
161
169
  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.`);
170
+ setDone(`Accepted ${plural(Number(res.count ?? names.length), "mailbox", "mailboxes")}; provisioning continues in the background.`);
163
171
  onPurchased?.(domainId);
164
172
  });
165
173
  const connected = listOf<DomainRow>(domains.data).filter((d) => d.provider_connected);
@@ -219,7 +227,7 @@ function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void
219
227
  </div>
220
228
  )}
221
229
  <Button disabled={!ready} onClick={() => setConfirming(true)}>
222
- Buy {names.length || ""} mailbox{names.length === 1 ? "" : "es"}
230
+ Buy {names.length || ""} {pluralWord(names.length, "mailbox", "mailboxes")}
223
231
  </Button>
224
232
  {buy.error ? <ApiError error={buy.error} onRetry={buy.reset} /> : null}
225
233
  {done ? (
@@ -231,8 +239,8 @@ function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void
231
239
  <ConfirmSpendDialog
232
240
  open
233
241
  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."
242
+ title={`Buy ${plural(names.length, "mailbox", "mailboxes")}: ${names.join(", ")}`}
243
+ description={`Mailboxes are billed monthly while they exist, plus warmup if enabled. This is an estimate: ${brand.linkLabel} has no balance read.`}
236
244
  estimatedCredits={names.length * perMailbox}
237
245
  confirmLabel="Buy mailboxes"
238
246
  confirming={buy.loading}
@@ -244,8 +252,10 @@ function BuyForm({ onPurchased }: { onPurchased?: (id: number | string) => void
244
252
  );
245
253
  }
246
254
 
247
- export function ConnectMailbox({ appUrl = GRAPH8_APP_MAILBOXES_URL, onConnected, onPurchased, className }: ConnectMailboxProps) {
248
- const [mode, setMode] = useState<Mode>("imap");
255
+ export function ConnectMailbox({ appUrl: appUrlProp, onConnected, onPurchased, initialMode = "imap", domainId, className }: ConnectMailboxProps) {
256
+ const brand = useBrand();
257
+ const appUrl = appUrlProp ?? brandUrl(brand, GRAPH8_APP_MAILBOXES_PATH);
258
+ const [mode, setMode] = useState<Mode>(initialMode);
249
259
  return (
250
260
  <div className={`space-y-4 ${className ?? ""}`}>
251
261
  <div className="inline-flex gap-1" role="group" aria-label="How to add a mailbox">
@@ -268,17 +278,17 @@ export function ConnectMailbox({ appUrl = GRAPH8_APP_MAILBOXES_URL, onConnected,
268
278
  {mode === "oauth" ? (
269
279
  <div className="space-y-2 rounded border border-gray-200 p-3 text-sm">
270
280
  <p>
271
- Google and Microsoft mailboxes connect through a sign-in screen that only runs inside the graph8 app. Connect it there, then
281
+ Google and Microsoft mailboxes connect through a sign-in screen that only runs inside the {brand.linkLabel} app. Connect it there, then
272
282
  come back: it appears in your mailbox list.
273
283
  </p>
274
284
  <Button asChild>
275
285
  <a href={appUrl} target="_blank" rel="noreferrer">
276
- Open graph8 to connect
286
+ Open {brand.linkLabel} to connect
277
287
  </a>
278
288
  </Button>
279
289
  </div>
280
290
  ) : null}
281
- {mode === "buy" ? <BuyForm onPurchased={onPurchased} /> : null}
291
+ {mode === "buy" ? <BuyForm onPurchased={onPurchased} domainId={domainId} /> : null}
282
292
  </div>
283
293
  );
284
294
  }