@graph8/sdk 0.370.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 (466) hide show
  1. package/README.md +19 -0
  2. package/blocks/_foundation/README.md +14 -0
  3. package/blocks/_foundation/block.json +44 -0
  4. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  5. package/blocks/_foundation/components/empty-state.tsx +5 -2
  6. package/blocks/_foundation/components/list-select.tsx +112 -0
  7. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  8. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  9. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  10. package/blocks/_foundation/components/screen/index.ts +1 -0
  11. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  12. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  13. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  14. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  15. package/blocks/_foundation/lib/paging.ts +57 -0
  16. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  17. package/blocks/agents/README.md +81 -4
  18. package/blocks/agents/block.json +311 -2
  19. package/blocks/agents/components/agent-activity.tsx +112 -0
  20. package/blocks/agents/components/agent-approvals.tsx +135 -0
  21. package/blocks/agents/components/agent-builder.tsx +17 -3
  22. package/blocks/agents/components/agent-conversations.tsx +143 -0
  23. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  24. package/blocks/agents/components/agent-operators.tsx +164 -0
  25. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  26. package/blocks/agents/components/agent-runs.tsx +3 -3
  27. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  28. package/blocks/agents/components/agent-test-history.tsx +121 -0
  29. package/blocks/agents/components/agents-screen.tsx +53 -11
  30. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  31. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  32. package/blocks/agents/components/from-template-button.tsx +65 -0
  33. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  34. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  35. package/blocks/agents/components/knowledge-form.tsx +69 -0
  36. package/blocks/agents/components/name-dialog.tsx +32 -0
  37. package/blocks/agents/components/receptionist-button.tsx +46 -0
  38. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  39. package/blocks/agents/components/skill-editor.tsx +147 -0
  40. package/blocks/agents/components/skill-library.tsx +111 -0
  41. package/blocks/agents/lib/agent-types.ts +7 -1
  42. package/blocks/agents/lib/facts.tsx +13 -0
  43. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  44. package/blocks/agents/lib/load-more.tsx +141 -0
  45. package/blocks/agents/lib/skill-utils.ts +91 -0
  46. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  47. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  48. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  49. package/blocks/analytics/components/report-builder.tsx +15 -2
  50. package/blocks/appointments/README.md +6 -0
  51. package/blocks/appointments/block.json +27 -0
  52. package/blocks/appointments/components/booking-page.tsx +2 -2
  53. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  54. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  55. package/blocks/appointments/components/meeting-list.tsx +29 -13
  56. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  57. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  58. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  59. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  60. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  61. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  62. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  63. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  64. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  65. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  66. package/blocks/coach/components/call-scorecard.tsx +7 -7
  67. package/blocks/company-intelligence/README.md +132 -0
  68. package/blocks/company-intelligence/block.json +318 -0
  69. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  70. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  71. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  72. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  73. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  74. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  75. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  76. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  77. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  78. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  79. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  80. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  81. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  82. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  83. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  84. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  85. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  86. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  87. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  88. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  89. package/blocks/crm/README.md +6 -0
  90. package/blocks/crm/block.json +53 -4
  91. package/blocks/crm/components/companies-screen.tsx +23 -9
  92. package/blocks/crm/components/company-detail.tsx +2 -2
  93. package/blocks/crm/components/contact-detail.tsx +4 -4
  94. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  95. package/blocks/crm/components/contacts-screen.tsx +31 -13
  96. package/blocks/crm/components/dedupe-review.tsx +6 -1
  97. package/blocks/crm/components/import-wizard.tsx +3 -7
  98. package/blocks/crm/components/list-select.tsx +20 -0
  99. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  100. package/blocks/crm/components/record-form.tsx +1 -1
  101. package/blocks/crm/components/tasks-screen.tsx +35 -15
  102. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  103. package/blocks/crm/hooks/use-page-query.ts +3 -1
  104. package/blocks/crm/lib/crm-types.ts +14 -0
  105. package/blocks/crm/lib/read-lists.ts +16 -0
  106. package/blocks/deals/README.md +24 -6
  107. package/blocks/deals/block.json +151 -2
  108. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  109. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  110. package/blocks/deals/components/deal-detail.tsx +2 -2
  111. package/blocks/deals/components/deal-filters.tsx +65 -0
  112. package/blocks/deals/components/deal-line-items.tsx +120 -0
  113. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  114. package/blocks/deals/components/deal-related.tsx +30 -9
  115. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  116. package/blocks/deals/components/deals-screen.tsx +57 -22
  117. package/blocks/deals/components/forecast-view.tsx +4 -4
  118. package/blocks/deals/components/pipeline-board.tsx +8 -6
  119. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  120. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  121. package/blocks/deals/components/stage-editor.tsx +1 -1
  122. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  123. package/blocks/deals/lib/deal-extras.ts +156 -0
  124. package/blocks/dialer/README.md +14 -5
  125. package/blocks/dialer/block.json +17 -2
  126. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  127. package/blocks/dialer/components/call-log-screen.tsx +4 -1
  128. package/blocks/dialer/components/call-log.tsx +16 -4
  129. package/blocks/dialer/components/call-queue.tsx +3 -3
  130. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  131. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  132. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  133. package/blocks/dialer/components/session-start-form.tsx +58 -11
  134. package/blocks/dialer/components/softphone.tsx +1 -1
  135. package/blocks/enrichment/README.md +16 -6
  136. package/blocks/enrichment/block.json +19 -7
  137. package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
  138. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  139. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  140. package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
  141. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  142. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  143. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  144. package/blocks/forms/README.md +8 -0
  145. package/blocks/forms/block.json +107 -0
  146. package/blocks/forms/components/form-activity.tsx +71 -0
  147. package/blocks/forms/components/form-backfill.tsx +108 -0
  148. package/blocks/forms/components/form-builder.tsx +1 -1
  149. package/blocks/forms/components/form-embed.tsx +1 -1
  150. package/blocks/forms/components/form-groups.tsx +151 -0
  151. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  152. package/blocks/forms/components/form-route-history.tsx +72 -0
  153. package/blocks/forms/components/form-saved-views.tsx +81 -0
  154. package/blocks/forms/components/forms-screen.tsx +51 -17
  155. package/blocks/forms/components/more-rows.tsx +30 -0
  156. package/blocks/forms/components/submissions-export.tsx +67 -0
  157. package/blocks/forms/components/submissions-table.tsx +15 -6
  158. package/blocks/forms/lib/forms-extra.ts +67 -0
  159. package/blocks/inbox/README.md +5 -0
  160. package/blocks/inbox/block.json +15 -0
  161. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  162. package/blocks/inbox/components/thread-badge.tsx +2 -2
  163. package/blocks/inbox/components/thread-view.tsx +1 -1
  164. package/blocks/integrations/README.md +54 -1
  165. package/blocks/integrations/block.json +189 -15
  166. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  167. package/blocks/integrations/components/connection-health.tsx +59 -0
  168. package/blocks/integrations/components/crm-detail.tsx +20 -1
  169. package/blocks/integrations/components/crm-records.tsx +103 -0
  170. package/blocks/integrations/components/field-mappings.tsx +61 -0
  171. package/blocks/integrations/components/integration-list.tsx +4 -4
  172. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  173. package/blocks/integrations/components/migration-panel.tsx +133 -0
  174. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  175. package/blocks/integrations/components/sync-status.tsx +6 -6
  176. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  177. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  178. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  179. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  180. package/blocks/integrations/lib/crm-utils.ts +39 -0
  181. package/blocks/integrations/lib/facts.tsx +13 -0
  182. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  183. package/blocks/integrations/lib/load-more.tsx +141 -0
  184. package/blocks/intent/components/account-intent-score.tsx +5 -5
  185. package/blocks/intent/components/intent-feed.tsx +8 -8
  186. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  187. package/blocks/intent/lib/intent-utils.ts +4 -4
  188. package/blocks/lifecycle/components/account-related.tsx +8 -1
  189. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  190. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  191. package/blocks/lists/README.md +7 -1
  192. package/blocks/lists/block.json +31 -3
  193. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  194. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  195. package/blocks/lists/components/list-picker.tsx +13 -3
  196. package/blocks/lists/components/list-table.tsx +1 -1
  197. package/blocks/lists/components/lists-screen.tsx +4 -2
  198. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  199. package/blocks/lists/components/suppression-manager.tsx +27 -6
  200. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  201. package/blocks/mailboxes/block.json +4 -0
  202. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  203. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  204. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  205. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  206. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  207. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  208. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  209. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  210. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  211. package/blocks/newsletter/README.md +12 -0
  212. package/blocks/newsletter/block.json +117 -0
  213. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  214. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  215. package/blocks/newsletter/components/more-rows.tsx +30 -0
  216. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  217. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  218. package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
  219. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  220. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  221. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  222. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  223. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  224. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  225. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  226. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  227. package/blocks/nurture/components/nurture-form.tsx +5 -8
  228. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  229. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  230. package/blocks/objects/README.md +1 -1
  231. package/blocks/objects/block.json +13 -5
  232. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  233. package/blocks/objects/components/object-record-detail.tsx +8 -1
  234. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  235. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  236. package/blocks/pages/README.md +14 -0
  237. package/blocks/pages/block.json +113 -1
  238. package/blocks/pages/components/more-rows.tsx +30 -0
  239. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  240. package/blocks/pages/components/page-analytics.tsx +176 -0
  241. package/blocks/pages/components/page-clone.tsx +99 -0
  242. package/blocks/pages/components/page-domains.tsx +167 -0
  243. package/blocks/pages/components/page-editor.tsx +1 -1
  244. package/blocks/pages/components/page-generation-status.tsx +48 -0
  245. package/blocks/pages/components/page-tracking.tsx +137 -0
  246. package/blocks/pages/components/pages-screen.tsx +57 -16
  247. package/blocks/pages/components/seo-report.tsx +6 -1
  248. package/blocks/pages/lib/studio.ts +7 -0
  249. package/blocks/phone-numbers/README.md +74 -0
  250. package/blocks/phone-numbers/block.json +262 -0
  251. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  252. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  253. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  254. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  255. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  256. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  257. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  258. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  259. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  260. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  261. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  262. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  263. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  264. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  265. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  266. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  267. package/blocks/pickers/README.md +23 -0
  268. package/blocks/pickers/block.json +103 -0
  269. package/blocks/pickers/components/company-picker.tsx +90 -0
  270. package/blocks/pickers/components/contact-picker.tsx +131 -0
  271. package/blocks/pickers/components/member-picker.tsx +99 -0
  272. package/blocks/pickers/components/search-picker.tsx +98 -0
  273. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  274. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  275. package/blocks/pickers/lib/use-debounced.ts +13 -0
  276. package/blocks/quotes/README.md +13 -0
  277. package/blocks/quotes/block.json +48 -1
  278. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  279. package/blocks/quotes/components/quote-builder.tsx +1 -1
  280. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  281. package/blocks/quotes/components/quote-list.tsx +5 -3
  282. package/blocks/quotes/components/quote-related.tsx +3 -2
  283. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  284. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  285. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  286. package/blocks/radar/components/radar-overview.tsx +4 -4
  287. package/blocks/radar/components/radar-screen.tsx +2 -2
  288. package/blocks/sequencer/README.md +6 -0
  289. package/blocks/sequencer/block.json +28 -1
  290. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  291. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  292. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  293. package/blocks/sequencer/components/sequence-builder.tsx +19 -20
  294. package/blocks/sequencer/components/sequence-detail.tsx +4 -2
  295. package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
  296. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  297. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  298. package/blocks/sequencer/components/sequences-screen.tsx +3 -0
  299. package/blocks/sequencer/components/step-editor.tsx +11 -11
  300. package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
  301. package/blocks/social-ads/README.md +8 -0
  302. package/blocks/social-ads/block.json +137 -4
  303. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  304. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  305. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  306. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  307. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  308. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  309. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  310. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  311. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  312. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  313. package/blocks/social-ads/components/post-composer.tsx +4 -2
  314. package/blocks/social-ads/components/post-detail.tsx +3 -0
  315. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  316. package/blocks/social-ads/components/social-performance.tsx +80 -0
  317. package/blocks/social-ads/components/social-screen.tsx +33 -6
  318. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  319. package/blocks/surveys/README.md +91 -0
  320. package/blocks/surveys/block.json +299 -0
  321. package/blocks/surveys/components/account-nps.tsx +69 -0
  322. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  323. package/blocks/surveys/components/org-summary.tsx +82 -0
  324. package/blocks/surveys/components/sending-domains.tsx +113 -0
  325. package/blocks/surveys/components/survey-form.tsx +126 -0
  326. package/blocks/surveys/components/survey-responses.tsx +145 -0
  327. package/blocks/surveys/components/survey-results.tsx +72 -0
  328. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  329. package/blocks/surveys/components/survey-send.tsx +162 -0
  330. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  331. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  332. package/blocks/surveys/lib/surveys-types.ts +228 -0
  333. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  334. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  335. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  336. package/blocks/visitors/components/snippet-install.tsx +18 -9
  337. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  338. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  339. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  340. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  341. package/blocks/webchat/components/conversation-list.tsx +1 -1
  342. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  343. package/blocks/workbench/components/run-progress.tsx +1 -1
  344. package/blocks/workbench/components/workbench-grid.tsx +1 -1
  345. package/blocks/workbench/components/workbench-screen.tsx +15 -5
  346. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  347. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  348. package/dist/cli.js +536 -392
  349. package/dist/index.browser.js +1 -1
  350. package/dist/index.browser.js.map +1 -1
  351. package/dist/index.browser.mjs +1 -1
  352. package/dist/index.browser.mjs.map +1 -1
  353. package/dist/index.d.mts +2 -2
  354. package/dist/index.d.ts +2 -2
  355. package/dist/index.js +1 -1
  356. package/dist/index.js.map +1 -1
  357. package/dist/index.mjs +1 -1
  358. package/dist/index.mjs.map +1 -1
  359. package/dist/react.browser.js +1 -1
  360. package/dist/react.browser.js.map +1 -1
  361. package/dist/react.browser.mjs +1 -1
  362. package/dist/react.browser.mjs.map +1 -1
  363. package/dist/react.d.mts +2 -2
  364. package/dist/react.d.ts +2 -2
  365. package/dist/react.js +1 -1
  366. package/dist/react.js.map +1 -1
  367. package/dist/react.mjs +1 -1
  368. package/dist/react.mjs.map +1 -1
  369. package/dist/types/blocks/brand.d.ts +11 -2
  370. package/dist/types/blocks/create.d.ts +7 -3
  371. package/dist/types/blocks/doctor.d.ts +1 -0
  372. package/dist/types/blocks/flows.d.ts +7 -1
  373. package/dist/types/blocks/orgBrand.d.ts +34 -0
  374. package/dist/types/cli.d.ts +3 -0
  375. package/dist/types/version.d.ts +2 -2
  376. package/docs/api/desk.d.ts +2 -2
  377. package/docs/api/desk.md +3 -3
  378. package/package.json +1 -1
  379. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  380. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  381. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  382. package/stacks/6sense.json +17 -4
  383. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  384. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  385. package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
  386. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  387. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  388. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  389. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  390. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  391. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  392. package/stacks/apollo.json +36 -8
  393. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  394. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  395. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  396. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  397. package/stacks/calendly.json +8 -4
  398. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  399. package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
  400. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  401. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  402. package/stacks/clay.json +16 -3
  403. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  404. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  405. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  406. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  407. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  408. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  409. package/stacks/gong.json +23 -6
  410. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  411. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  412. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  413. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  414. package/stacks/hootsuite.json +9 -4
  415. package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
  416. package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
  417. package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
  418. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  419. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  420. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  421. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  422. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  423. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  424. package/stacks/hubspot.json +21 -5
  425. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  426. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  427. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  428. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  429. package/stacks/intercom.json +8 -4
  430. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  431. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  432. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  433. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  434. package/stacks/mailchimp.json +20 -5
  435. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  436. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  437. package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
  438. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  439. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  440. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  441. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  442. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  443. package/stacks/outreach.json +25 -7
  444. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  445. package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
  446. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  447. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  448. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  449. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  450. package/stacks/pipedrive.json +4 -2
  451. package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
  452. package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
  453. package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
  454. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  455. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  456. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  457. package/stacks/salesforce.json +7 -3
  458. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  459. package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
  460. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  461. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  462. package/stacks/zoominfo.json +16 -3
  463. package/styles/styles.css +1 -1
  464. package/templates/next-app/app/globals.css.tmpl +349 -64
  465. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  466. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SugarCrmConnect: connect SugarCRM from here. Unlike HubSpot, Salesforce, Pipedrive and Zoho (whose sign-in screen only runs in the graph8
5
+ * app, so those link out), SugarCRM signs in with an instance address, a user and a password, which the API can take directly.
6
+ * Not connected the form; Connect signs in and stores the credentials (there is no way to check them before saving).
7
+ * Connected "Check connection" (free, reads the stored login), "Update credentials" (sign in again with new ones), "Remove".
8
+ * Connecting and updating sign in to your SugarCRM and store the credentials, so they go through ConfirmSpendDialog (external);
9
+ * removing is destructive and confirmed. The password is sent once through your server's proxy and is cleared from the form afterwards.
10
+ *
11
+ * SDK operations, in order:
12
+ * 1. validateSugarcrm POST /integrations/sugarcrm/validate (read) checks the stored credentials
13
+ * 2. connectSugarcrm POST /integrations/sugarcrm/connect { instance_url, username, password, client_id?, client_secret? } (external, confirmed)
14
+ * 3. reconnectSugarcrm POST /integrations/sugarcrm/reconnect { same } (external, confirmed)
15
+ * 4. disconnectSugarcrm DELETE /integrations/sugarcrm (destructive, confirmed)
16
+ */
17
+ import { useState } from "react";
18
+ import { Button, Input, Label } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { g8Delete, g8Post } from "@/g8/lib/g8-client";
23
+ import { useBrand } from "@/g8/hooks/use-brand";
24
+
25
+ export interface SugarCrmConnectProps {
26
+ /** Whether SugarCRM is connected now (from the sync list). */
27
+ connected: boolean;
28
+ /** After a connect, update or removal. */
29
+ onChanged?: () => void;
30
+ className?: string;
31
+ }
32
+
33
+ interface Form { instance_url: string; username: string; password: string; client_id: string; client_secret: string }
34
+ const BLANK: Form = { instance_url: "", username: "", password: "", client_id: "", client_secret: "" };
35
+ type Ask = "connect" | "reconnect" | "remove";
36
+
37
+ /** The request body: client id and secret only when typed (the instance's defaults apply otherwise). */
38
+ export function sugarBody(f: Form): Record<string, string> {
39
+ return { instance_url: f.instance_url.trim().replace(/\/+$/, ""), username: f.username.trim(), password: f.password, ...(f.client_id.trim() ? { client_id: f.client_id.trim() } : {}), ...(f.client_secret ? { client_secret: f.client_secret } : {}) };
40
+ }
41
+
42
+ export function SugarCrmConnect({ connected, onChanged, className }: SugarCrmConnectProps) {
43
+ const brand = useBrand();
44
+ const [f, setF] = useState<Form>(BLANK);
45
+ const [ask, setAsk] = useState<Ask | null>(null);
46
+ const [showAdvanced, setShowAdvanced] = useState(false);
47
+ const [editing, setEditing] = useState(false);
48
+ const set = (patch: Partial<Form>) => setF((x) => ({ ...x, ...patch }));
49
+ const valid = /^https?:\/\/\S+$/i.test(f.instance_url.trim()) && f.username.trim() && f.password;
50
+ const done = () => { setF(BLANK); setAsk(null); setEditing(false); onChanged?.(); };
51
+
52
+ const connect = useG8Mutation((kind: "connect" | "reconnect") => g8Post(`/integrations/sugarcrm/${kind}`, sugarBody(f)), { onSuccess: done, onError: () => setAsk(null) });
53
+ const check = useG8Mutation(() => g8Post("/integrations/sugarcrm/validate", {}));
54
+ const remove = useG8Mutation(() => g8Delete("/integrations/sugarcrm"), { onSuccess: done, onError: () => setAsk(null) });
55
+ const verdict = (check.data as { data?: { valid?: boolean; connected?: boolean; message?: string } } | undefined)?.data;
56
+ const showForm = !connected || editing;
57
+
58
+ return (
59
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
60
+ {connected ? (
61
+ <div className="flex flex-wrap items-center gap-2">
62
+ <Button size="sm" variant="outline" loading={check.loading} onClick={() => void check.mutate(undefined)}>Check connection</Button>
63
+ <Button size="sm" variant="outline" onClick={() => setEditing((v) => !v)}>{editing ? "Close" : "Update credentials"}</Button>
64
+ <Button size="sm" variant="ghost" onClick={() => setAsk("remove")}>Remove</Button>
65
+ {check.data !== undefined ? <span role="status" className="text-sm" data-testid="sugar-check">{verdict?.valid === false || verdict?.connected === false ? `Sign-in failed${verdict?.message ? `: ${verdict.message}` : ""}` : "Signed in to SugarCRM."}</span> : null}
66
+ </div>
67
+ ) : <p className="text-sm text-muted-foreground">Sign in to your SugarCRM instance to sync contacts, companies and deals.</p>}
68
+ {check.error ? <ApiError error={check.error} title="Could not check the connection" /> : null}
69
+ {showForm ? (
70
+ <form className="flex flex-col gap-3" onSubmit={(e) => { e.preventDefault(); if (valid) setAsk(connected ? "reconnect" : "connect"); }}>
71
+ <div className="flex flex-col gap-1"><Label htmlFor="sugar-url">Instance address</Label><Input id="sugar-url" type="url" placeholder="https://yourcompany.sugarondemand.com" value={f.instance_url} onChange={(e) => set({ instance_url: e.target.value })} /></div>
72
+ <div className="grid gap-3 sm:grid-cols-2">
73
+ <div className="flex flex-col gap-1"><Label htmlFor="sugar-user">User</Label><Input id="sugar-user" autoComplete="off" value={f.username} onChange={(e) => set({ username: e.target.value })} /></div>
74
+ <div className="flex flex-col gap-1"><Label htmlFor="sugar-pass">Password</Label><Input id="sugar-pass" type="password" autoComplete="new-password" value={f.password} onChange={(e) => set({ password: e.target.value })} /></div>
75
+ </div>
76
+ <button type="button" className="self-start text-xs underline" onClick={() => setShowAdvanced((v) => !v)}>{showAdvanced ? "Hide" : "Show"} client credentials</button>
77
+ {showAdvanced ? (
78
+ <div className="grid gap-3 sm:grid-cols-2">
79
+ <div className="flex flex-col gap-1"><Label htmlFor="sugar-cid">Client id (optional)</Label><Input id="sugar-cid" autoComplete="off" value={f.client_id} onChange={(e) => set({ client_id: e.target.value })} /></div>
80
+ <div className="flex flex-col gap-1"><Label htmlFor="sugar-secret">Client secret (optional)</Label><Input id="sugar-secret" type="password" autoComplete="new-password" value={f.client_secret} onChange={(e) => set({ client_secret: e.target.value })} /></div>
81
+ </div>
82
+ ) : null}
83
+ {connect.error ? <ApiError error={connect.error} title={connected ? "Could not update the credentials" : "Could not connect SugarCRM"} /> : null}
84
+ <div><Button type="submit" disabled={!valid}>{connected ? "Save credentials" : "Connect SugarCRM"}</Button></div>
85
+ </form>
86
+ ) : null}
87
+ {remove.error ? <ApiError error={remove.error} title="Could not remove the connection" /> : null}
88
+ <ConfirmSpendDialog open={ask === "connect" || ask === "reconnect"} tier="external" title={ask === "reconnect" ? "Update the SugarCRM credentials" : "Connect SugarCRM"}
89
+ description={`${brand.name} signs in to ${f.instance_url.trim() || "your instance"} as ${f.username.trim() || "this user"} and stores the credentials to sync on your behalf.`}
90
+ confirmLabel={ask === "reconnect" ? "Update" : "Connect"} confirming={connect.loading} onConfirm={() => ask && ask !== "remove" && void connect.mutate(ask)} onCancel={() => setAsk(null)} />
91
+ <ConfirmSpendDialog open={ask === "remove"} tier="destructive" title="Remove the SugarCRM connection" description="Syncs configured against it stop working and the stored credentials are deleted. Records already synced stay."
92
+ confirmLabel="Remove" confirming={remove.loading} onConfirm={() => void remove.mutate(undefined)} onCancel={() => setAsk(null)} />
93
+ </div>
94
+ );
95
+ }
@@ -51,7 +51,7 @@ function JobProgress({ jobId }: { jobId: string }) {
51
51
  },
52
52
  );
53
53
  return (
54
- <p role="status" className="text-sm text-gray-700" data-testid="sync-job">
54
+ <p role="status" className="text-sm text-foreground" data-testid="sync-job">
55
55
  Job {jobId}: {job.status?.status ?? "starting"}
56
56
  {job.polling ? "..." : ""}
57
57
  {job.error ? " (could not read the job status)" : ""}
@@ -117,7 +117,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
117
117
  const s = status.data?.data;
118
118
  if (!s?.connected) {
119
119
  return (
120
- <p role="status" className={`rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900 ${className ?? ""}`}>
120
+ <p role="status" className={`rounded border border-warning-foreground/25 bg-warning p-3 text-sm text-warning-foreground ${className ?? ""}`}>
121
121
  {label} is not connected. {s?.message ?? `Connect it in ${brand.linkLabel}, then come back.`}
122
122
  </p>
123
123
  );
@@ -145,7 +145,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
145
145
  <div className="flex items-center justify-between">
146
146
  <div>
147
147
  <h3 className="font-medium">{label}</h3>
148
- <p className="text-xs text-gray-500">Connected. Last sync: {formatWhen(s.last_sync_at)}</p>
148
+ <p className="text-xs text-muted-foreground">Connected. Last sync: {formatWhen(s.last_sync_at)}</p>
149
149
  </div>
150
150
  <Button size="sm" variant="outline" onClick={() => void status.refetch()}>
151
151
  Recheck
@@ -153,7 +153,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
153
153
  </div>
154
154
 
155
155
  {(["push", "pull"] as const).map((direction) => (
156
- <div key={direction} className="flex items-center justify-between rounded border border-gray-200 px-3 py-2 text-sm">
156
+ <div key={direction} className="flex items-center justify-between rounded border border-border px-3 py-2 text-sm">
157
157
  <span>{direction === "push" ? `Send ${brand.linkLabel} contacts to the CRM` : `Bring CRM contacts into ${brand.linkLabel}`}</span>
158
158
  <span className="flex gap-2">
159
159
  <Button
@@ -181,7 +181,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
181
181
  <Button onClick={() => setPending({ kind: "push" })}>Push {records.length} contacts now</Button>
182
182
  ) : null}
183
183
  {pushed ? (
184
- <p role="status" className="text-sm text-green-700">
184
+ <p role="status" className="text-sm text-positive-foreground">
185
185
  {pushed}
186
186
  </p>
187
187
  ) : null}
@@ -199,7 +199,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
199
199
  ) : (
200
200
  <ul className="mt-1 flex flex-wrap gap-1 text-xs">
201
201
  {(fields.data ?? []).map((f, i) => (
202
- <li key={i} className="rounded border border-gray-200 px-1.5 py-0.5">
202
+ <li key={i} className="rounded border border-border px-1.5 py-0.5">
203
203
  {f.label ?? f.name}
204
204
  </li>
205
205
  ))}
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SyncTriggers: run a sync now instead of waiting for the continuous one. For one connected CRM:
5
+ * Contacts send every graph8 contact to the CRM, or bring the CRM's contacts in (optionally only the first N).
6
+ * Deals the same for deals.
7
+ * Leads Salesforce only (the lead routes are Salesforce-specific). Check what a push would send (a free preview), choose which lead
8
+ * status qualified and unqualified contacts get, then send leads. A large push needs an explicit acknowledgement of the previewed
9
+ * count (large_population_confirmed). Bringing leads in runs the organisation's SAVED lead pull settings (only connection_id and
10
+ * an optional limit are sent, so the saved scope is not overridden).
11
+ * Each sync is a background job: it appears under "Jobs" and is followed (polled) until it ends. Every sync changes data in the CRM
12
+ * or in graph8 and can touch thousands of records, so each one goes through ConfirmSpendDialog (external) that states its scope.
13
+ *
14
+ * SDK operations, in order:
15
+ * 1. pushAllContacts / pullAllContacts POST /integrations/crm-sync/contacts/push|pull { connection_id, limit? } (external, confirmed)
16
+ * 2. pushAllDeals / pullAllDeals POST /integrations/crm-sync/deals/push|pull { connection_id, limit? } (external, confirmed)
17
+ * 3. listLeadStatuses GET /integrations/crm-sync/leads/statuses?connection_id (read)
18
+ * 4. previewLeadPush GET /integrations/crm-sync/leads/push-preview?connection_id&source_mode&... (read)
19
+ * 5. pushAllLeads / pullAllLeads POST /integrations/crm-sync/leads/push|pull { connection_id, source_mode, ... } (external, confirmed)
20
+ * 6. getSyncJob GET /integrations/crm-sync/jobs/{job_id} (read, polled)
21
+ * Syncing a graph8 list or audience needs a list picker and is started from the Lists screen.
22
+ */
23
+ import { useEffect, useState } from "react";
24
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
25
+ import { ApiError } from "@/g8/components/api-error";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { SectionCard } from "@/g8/components/screen/section-card";
28
+ import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
29
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
30
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
31
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
32
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
33
+ import { PROVIDER_LABELS, TERMINAL_JOB_STATES } from "../lib/integrations-utils";
34
+ import { useBrand } from "@/g8/hooks/use-brand";
35
+ import type { CrmProvider } from "../lib/integrations-utils";
36
+
37
+ export interface SyncTriggersProps {
38
+ connectionId: string;
39
+ provider: CrmProvider;
40
+ className?: string;
41
+ }
42
+
43
+ interface Job { id: string; label: string }
44
+ interface Plan { path: string; label: string; scope: string; body: Record<string, unknown> }
45
+ interface JobStatus { job_id?: string | null; status?: string | null; processed?: number | null; total?: number | null; failed?: number | null }
46
+
47
+ function JobRow({ job }: { job: Job }) {
48
+ const poll = useJobPoll<JobStatus>(async (signal) => (await g8Get<JobStatus>(`/integrations/crm-sync/jobs/${encodeURIComponent(job.id)}`, undefined, { signal })).data, { isTerminal: (j) => TERMINAL_JOB_STATES.includes(String(j.status ?? "").toLowerCase()) });
49
+ const st = String(poll.status?.status ?? "starting");
50
+ const s = poll.status;
51
+ return (
52
+ <li className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm" data-testid={`job-${job.id}`}>
53
+ <span>{job.label}<span className="ml-2 text-xs text-muted-foreground">{job.id}</span></span>
54
+ <span className="flex items-center gap-2">
55
+ {s?.total ? <span className="text-xs text-muted-foreground">{s.processed ?? 0} of {s.total}{s.failed ? `, ${s.failed} failed` : ""}</span> : null}
56
+ <StatusPill tone={toneForStatus(st)} dot>{st}</StatusPill>{poll.polling ? <span className="text-xs text-muted-foreground">updating</span> : null}{poll.error ? <span className="text-xs text-error-foreground">could not read the status</span> : null}
57
+ </span>
58
+ </li>
59
+ );
60
+ }
61
+
62
+ export function SyncTriggers({ connectionId, provider, className }: SyncTriggersProps) {
63
+ const brand = useBrand();
64
+ const label = PROVIDER_LABELS[provider];
65
+ const [bigOk, setBigOk] = useState(false);
66
+ const [limit, setLimit] = useState("");
67
+ const [mode, setMode] = useState<"all_qualified" | "all_contacts">("all_qualified");
68
+ const [qualified, setQualified] = useState("");
69
+ const [unqualified, setUnqualified] = useState("");
70
+ const [plan, setPlan] = useState<Plan | null>(null);
71
+ const [jobs, setJobs] = useState<Job[]>([]);
72
+ useEffect(() => { setPlan(null); setJobs([]); setLimit(""); setQualified(""); setUnqualified(""); setBigOk(false); }, [connectionId]);
73
+
74
+ const statuses = useG8Query((signal) => g8Get("/integrations/crm-sync/leads/statuses", { connection_id: connectionId }, { signal }), [connectionId], { enabled: provider === "salesforce" });
75
+ const statusRows = (((statuses.data as { data?: { statuses?: Array<string | { value?: string; id?: string; label?: string; name?: string }> } } | undefined)?.data?.statuses) ?? []).map((s) => typeof s === "string" ? { value: s, label: s } : { value: s.value ?? s.id ?? s.name ?? "", label: s.label ?? s.name ?? s.value ?? s.id ?? "" });
76
+ const previewKey = `${connectionId}|${mode}|${qualified}|${unqualified}`;
77
+ const [previewed, setPreviewed] = useState("");
78
+ const preview = useG8Query((signal) => g8Get("/integrations/crm-sync/leads/push-preview", { connection_id: connectionId, source_mode: mode, status_qualified: qualified || undefined, status_unqualified: unqualified || undefined }, { signal }), [previewKey], { enabled: false });
79
+
80
+ const n = Number(limit);
81
+ const limitOk = limit.trim() === "" || (Number.isInteger(n) && n > 0);
82
+ const cap = limit.trim() ? { limit: n } : {};
83
+ const scope = limit.trim() ? `up to ${n.toLocaleString()}` : "all";
84
+ const run = useG8Mutation(async (p: Plan) => {
85
+ const res = await g8Post<{ job_id?: string | null }>(p.path, p.body);
86
+ return { id: res.data?.job_id ?? null, label: p.label };
87
+ }, { onSuccess: (r) => { setPlan(null); if (r.id) setJobs((j) => [{ id: r.id as string, label: r.label }, ...j]); }, onError: () => setPlan(null) });
88
+
89
+ const plans = (kind: "contacts" | "deals"): Plan[] => [
90
+ { path: `/integrations/crm-sync/${kind}/push`, label: `Send ${kind} to ${label}`, scope: `${scope} ${brand.name} ${kind}, created or updated in ${label}`, body: { connection_id: connectionId, ...cap } },
91
+ { path: `/integrations/crm-sync/${kind}/pull`, label: `Bring ${kind} in from ${label}`, scope: `${scope} ${label} ${kind}, created or updated in ${brand.name}`, body: { connection_id: connectionId, ...cap } },
92
+ ];
93
+ const leadPush: Plan = { path: "/integrations/crm-sync/leads/push", label: `Send leads to ${label}`, scope: `${mode === "all_qualified" ? "qualified" : "all"} contacts as leads in ${label}`,
94
+ body: { connection_id: connectionId, source_mode: mode, ...(qualified ? { status_qualified: qualified } : {}), ...(unqualified ? { status_unqualified: unqualified } : {}), ...(bigOk ? { large_population_confirmed: true } : {}), ...cap } };
95
+ const leadPull: Plan = { path: "/integrations/crm-sync/leads/pull", label: `Bring leads in from ${label}`, scope: `${scope} ${label} leads (the saved lead pull settings), created or updated in ${brand.name}`, body: { connection_id: connectionId, ...cap } };
96
+ const pv = ((preview.data as { data?: Record<string, unknown> } | undefined)?.data ?? {}) as Record<string, unknown>;
97
+ const shown = previewed === previewKey && preview.data !== undefined;
98
+ const row = (p: Plan) => <Button key={p.path} size="sm" variant="outline" disabled={!limitOk} onClick={() => setPlan(p)}>{p.label}</Button>;
99
+ return (
100
+ <div className={`flex flex-col gap-4 ${className ?? ""}`}>
101
+ <div className="flex flex-col gap-1 sm:max-w-xs">
102
+ <Label htmlFor="sync-limit">Only the first N records (optional)</Label>
103
+ <Input id="sync-limit" inputMode="numeric" placeholder="All" value={limit} onChange={(e) => setLimit(e.target.value)} />
104
+ {!limitOk ? <p role="alert" className="text-xs text-error-foreground">Enter a whole number above zero, or leave it empty.</p> : null}
105
+ </div>
106
+ <SectionCard title="Contacts" description={`Between ${brand.name} and ${label}`}><div className="flex flex-wrap gap-2">{plans("contacts").map(row)}</div></SectionCard>
107
+ <SectionCard title="Deals" description={`Between ${brand.name} and ${label}`}><div className="flex flex-wrap gap-2">{plans("deals").map(row)}</div></SectionCard>
108
+ {provider === "salesforce" ? <SectionCard title="Leads" description="Check what would be sent first, then send">
109
+ <div className="flex flex-col gap-3">
110
+ <div className="grid gap-3 sm:grid-cols-3">
111
+ <div className="flex flex-col gap-1"><Label htmlFor="lead-mode">Send</Label><select id="lead-mode" className="h-9 rounded-md border bg-background px-2 text-sm" value={mode} onChange={(e) => setMode(e.target.value as typeof mode)}><option value="all_qualified">Qualified contacts</option><option value="all_contacts">All contacts</option></select></div>
112
+ <div className="flex flex-col gap-1"><Label htmlFor="lead-q">Status for qualified</Label><select id="lead-q" className="h-9 rounded-md border bg-background px-2 text-sm" value={qualified} onChange={(e) => setQualified(e.target.value)}><option value="">CRM default</option>{statusRows.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}</select></div>
113
+ <div className="flex flex-col gap-1"><Label htmlFor="lead-u">Status for others</Label><select id="lead-u" className="h-9 rounded-md border bg-background px-2 text-sm" value={unqualified} onChange={(e) => setUnqualified(e.target.value)}><option value="">CRM default</option>{statusRows.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}</select></div>
114
+ </div>
115
+ {statuses.error ? <ApiError compact error={statuses.error} title="Could not load the lead statuses" onRetry={() => void statuses.refetch()} /> : null}
116
+ <div className="flex flex-wrap items-center gap-2">
117
+ <Button size="sm" variant="outline" loading={preview.loading} onClick={() => { setPreviewed(previewKey); void preview.refetch(); }}>Preview the push</Button>
118
+ {shown ? <span role="status" className="text-sm" data-testid="lead-preview">{Object.keys(pv).length ? Object.entries(pv).filter(([, v]) => typeof v === "number" || typeof v === "string").slice(0, 4).map(([k, v]) => `${k.replace(/_/g, " ")}: ${String(v)}`).join(", ") : "Nothing would be sent."}</span> : null}
119
+ </div>
120
+ {preview.error && previewed === previewKey ? <ApiError compact error={preview.error} title="Could not preview the push" /> : null}
121
+ <label className="flex items-start gap-2 text-sm"><input type="checkbox" className="mt-1" checked={bigOk} onChange={(e) => setBigOk(e.target.checked)} /><span>I have checked the previewed count and accept a large push (needed only above the app's population limit).</span></label>
122
+ <div className="flex flex-wrap gap-2">{row(leadPush)}{row(leadPull)}</div>
123
+ </div>
124
+ </SectionCard> : null}
125
+ {run.error ? <ApiError error={run.error} title="The sync did not start" /> : null}
126
+ <SectionCard title={`Jobs (${jobs.length})`} description="Started in this session" flush>
127
+ {run.loading ? <Skeleton className="m-3 h-8" /> : null}
128
+ {jobs.length === 0 ? <p className="px-4 py-3 text-sm text-muted-foreground">No sync started yet. Pick one above.</p> : <ul className="divide-y">{jobs.map((j) => <JobRow key={j.id} job={j} />)}</ul>}
129
+ </SectionCard>
130
+ <ConfirmSpendDialog open={plan != null} tier="external" title={plan?.label ?? "Run a sync"} description={`This changes real data: ${plan?.scope ?? ""}. It runs in the background and cannot be recalled.`}
131
+ confirmLabel="Start sync" confirming={run.loading} onConfirm={() => plan && void run.mutate(plan)} onCancel={() => setPlan(null)} />
132
+ </div>
133
+ );
134
+ }
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * SDK operations, in order:
9
9
  * 1. listWebhooks GET /webhooks (read)
10
+ * getWebhook GET /webhooks/{id} (read; only when the id is not in the list, e.g. a deep link to a webhook beyond the list's page)
10
11
  * 2. listWebhookEvents GET /webhooks/events (read; the event names to pick from)
11
12
  * 3. listWebhookDeliveries GET /webhooks/{id}/deliveries (read)
12
13
  * 4. updateWebhook PATCH /webhooks/{id} (external, confirmed; edit, pause, resume)
@@ -17,6 +18,9 @@
17
18
  import { useEffect, useState, type ReactNode } from "react";
18
19
  import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
19
20
  import { ApiError } from "@/g8/components/api-error";
21
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
22
+ import { byPage, hasNextOf } from "@/g8/lib/paging";
23
+ import { LoadMore } from "@/g8/components/paging-notices";
20
24
  import { EmptyState } from "@/g8/components/empty-state";
21
25
  import { useG8Query } from "@/g8/hooks/use-g8-query";
22
26
  import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
@@ -45,13 +49,24 @@ interface Hook { id: string; url: string; name?: string | null; events?: string[
45
49
  interface EventType { event: string; category?: string | null; description?: string | null }
46
50
  interface Delivery { id: string; event: string; status: string; attempts?: number; max_attempts?: number; response_code?: number | null; error_message?: string | null; created_at?: string | null; completed_at?: string | null }
47
51
 
52
+ /** Deliveries per read; "Load more" appends the next page. */
53
+ const DELIVERY_PAGE = 20;
54
+
48
55
  export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: WebhookDetailProps) {
49
56
  const brand = useBrand();
50
57
  const { ask, dialog, failure } = useConfirmAction();
51
58
  const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
52
59
  const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
53
- const deliveries = useG8Query((signal) => g8Get(`/webhooks/${webhookId}/deliveries`, { page: 1, limit: 20 }, { signal }), [webhookId]);
54
- const hook = arrayOf<Hook>(hooks.data).find((h) => h.id === webhookId);
60
+ const deliveries = useLoadMoreList(byPage<Delivery>(DELIVERY_PAGE, async (page, signal) => {
61
+ const env = await g8Get(`/webhooks/${webhookId}/deliveries`, { page, limit: DELIVERY_PAGE }, { signal });
62
+ const rows = arrayOf<Delivery>(env);
63
+ return { rows, hasNext: hasNextOf(env, DELIVERY_PAGE, rows.length) };
64
+ }), [webhookId]);
65
+ const listed = arrayOf<Hook>(hooks.data).find((h) => h.id === webhookId);
66
+ const single = useG8Query((signal) => g8Get<Hook>(`/webhooks/${webhookId}`, undefined, { signal }), [webhookId], { enabled: Boolean(hooks.data) && !listed });
67
+ const fetched = (single.data as { data?: Hook } | undefined)?.data;
68
+ // Only the record fetched for THIS id counts: useG8Query keeps the previous webhook's data while the next one loads.
69
+ const hook = listed ?? (fetched?.id === webhookId ? fetched : undefined);
55
70
  const [form, setForm] = useState({ url: "", name: "", events: [] as string[] });
56
71
  const [secret, setSecret] = useState<string | null>(null);
57
72
 
@@ -60,14 +75,15 @@ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChan
60
75
  }, [hook?.id, hook?.url, hook?.name, hook?.events?.join(",")]); // eslint-disable-line react-hooks/exhaustive-deps
61
76
 
62
77
  if (hooks.error) return <ApiError error={hooks.error} title="Could not load the webhook" onRetry={() => void hooks.refetch()} />;
63
- if (!hooks.data) return <Skeleton className="h-40 w-full" />;
78
+ if (!hook && single.error) return <ApiError error={single.error} title="Could not load the webhook" onRetry={() => void single.refetch()} />;
79
+ if (!hooks.data || (!hook && single.loading)) return <Skeleton className="h-40 w-full" />;
64
80
  if (!hook) return <EmptyState title="Webhook not found" description="It may have been deleted." />;
65
- const changed = () => { void hooks.refetch(); onChanged?.(); };
81
+ const changed = () => { void hooks.refetch(); if (!listed) void single.refetch(); onChanged?.(); };
66
82
  const active = hook.is_active !== false;
67
83
  const byCategory = arrayOf<EventType>(events.data).reduce<Record<string, EventType[]>>((acc, e) => ({ ...acc, [e.category ?? "other"]: [...(acc[e.category ?? "other"] ?? []), e] }), {});
68
84
  const toggle = (event: string) => setForm((f) => ({ ...f, events: f.events.includes(event) ? f.events.filter((x) => x !== event) : [...f.events, event] }));
69
85
  const urlOk = /^https:\/\/\S+$/.test(form.url.trim());
70
- const rows = arrayOf<Delivery>(deliveries.data);
86
+ const rows = deliveries.data ?? [];
71
87
 
72
88
  const actions = (
73
89
  <div className="flex flex-wrap gap-1">
@@ -88,9 +104,9 @@ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChan
88
104
  content: (
89
105
  <div className="space-y-4">
90
106
  {secret ? (
91
- <div role="alert" className="rounded border border-amber-300 bg-amber-50 p-3 text-sm">
107
+ <div role="alert" className="rounded border border-warning-foreground/30 bg-warning p-3 text-sm">
92
108
  <p className="font-medium">New signing secret. It is shown once.</p>
93
- <code className="mt-1 block break-all rounded bg-white p-2">{secret}</code>
109
+ <code className="mt-1 block break-all rounded bg-background p-2">{secret}</code>
94
110
  <p className="mt-1 text-xs">Store it as G8_WEBHOOK_SECRET on your server. Never put it in browser code.</p>
95
111
  <Button size="sm" variant="outline" className="mt-2" onClick={() => setSecret(null)}>I saved it</Button>
96
112
  </div>
@@ -137,6 +153,7 @@ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChan
137
153
  id: "deliveries",
138
154
  label: "Deliveries",
139
155
  content: deliveries.loading && !deliveries.data ? <Skeleton className="h-16 w-full" /> : deliveries.error ? <ApiError error={deliveries.error} onRetry={() => void deliveries.refetch()} /> : rows.length === 0 ? <p className="text-sm text-muted-foreground">No deliveries yet.</p> : (
156
+ <>
140
157
  <table className="w-full text-sm" aria-label="Deliveries">
141
158
  <thead><tr className="text-left text-muted-foreground"><th>Event</th><th>Status</th><th>Code</th><th>Attempts</th><th>When</th></tr></thead>
142
159
  <tbody>
@@ -147,6 +164,8 @@ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChan
147
164
  ))}
148
165
  </tbody>
149
166
  </table>
167
+ <LoadMore shown={rows.length} noun="deliveries" hasMore={deliveries.hasMore} loading={deliveries.loadingMore} onLoadMore={deliveries.loadMore} />
168
+ </>
150
169
  ),
151
170
  },
152
171
  ];
@@ -23,6 +23,9 @@ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
23
23
  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
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
27
+ import { byPage, hasNextOf } from "@/g8/lib/paging";
28
+ import { LoadMore } from "@/g8/components/paging-notices";
26
29
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
30
  import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
28
31
  import { EmptyState } from "@/g8/components/empty-state";
@@ -105,16 +108,24 @@ function copyFor(p: Pending, platform: string = DEFAULT_BRAND.linkLabel): { tier
105
108
  }
106
109
  }
107
110
 
111
+ /** Deliveries per read; "Load more" appends the next page. */
112
+ const DELIVERY_PAGE = 20;
113
+
108
114
  function Deliveries({ hook }: { hook: Webhook }) {
109
- const list = useG8Query((signal) => g8Get(`/webhooks/${hook.id}/deliveries`, { page: 1, limit: 20 }, { signal }), [hook.id]);
110
- if (list.loading) return <Skeleton className="h-16 w-full" />;
115
+ const list = useLoadMoreList(byPage<Delivery>(DELIVERY_PAGE, async (page, signal) => {
116
+ const env = await g8Get(`/webhooks/${hook.id}/deliveries`, { page, limit: DELIVERY_PAGE }, { signal });
117
+ const rows = arrayOf<Delivery>(env);
118
+ return { rows, hasNext: hasNextOf(env, DELIVERY_PAGE, rows.length) };
119
+ }), [hook.id]);
120
+ if (list.loading && !list.data) return <Skeleton className="h-16 w-full" />;
111
121
  if (list.error) return <ApiError error={list.error} onRetry={() => void list.refetch()} />;
112
- const rows = arrayOf<Delivery>(list.data);
113
- if (rows.length === 0) return <p className="text-sm text-gray-500">No deliveries yet.</p>;
122
+ const rows = list.data ?? [];
123
+ if (rows.length === 0) return <p className="text-sm text-muted-foreground">No deliveries yet.</p>;
114
124
  return (
125
+ <>
115
126
  <table className="w-full text-sm" aria-label="Deliveries">
116
127
  <thead>
117
- <tr className="text-left text-gray-500">
128
+ <tr className="text-left text-muted-foreground">
118
129
  <th>Event</th>
119
130
  <th>Status</th>
120
131
  <th>Code</th>
@@ -134,6 +145,8 @@ function Deliveries({ hook }: { hook: Webhook }) {
134
145
  ))}
135
146
  </tbody>
136
147
  </table>
148
+ <LoadMore shown={rows.length} noun="deliveries" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
149
+ </>
137
150
  );
138
151
  }
139
152
 
@@ -178,8 +191,8 @@ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }:
178
191
 
179
192
  return (
180
193
  <div className={`space-y-4 ${className ?? ""}`}>
181
- <section className="space-y-3 rounded border border-gray-200 p-3" aria-label="New webhook">
182
- <h3 className="text-sm font-semibold">New webhook</h3>
194
+ <section className="space-y-3 rounded border border-border p-3" aria-label="New webhook">
195
+ <h3 className="text-sm font-medium">New webhook</h3>
183
196
  <div className="grid gap-2 md:grid-cols-2">
184
197
  <Input
185
198
  aria-label="Endpoint URL"
@@ -189,14 +202,14 @@ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }:
189
202
  />
190
203
  <Input aria-label="Name" placeholder="Name (optional)" value={name} onChange={(e) => setName(e.target.value)} />
191
204
  </div>
192
- {url && !urlOk ? <p className="text-xs text-red-700">The endpoint must be an https:// URL.</p> : null}
205
+ {url && !urlOk ? <p className="text-xs text-error-foreground">The endpoint must be an https:// URL.</p> : null}
193
206
  {events.loading ? (
194
207
  <Skeleton className="h-12 w-full" />
195
208
  ) : (
196
209
  <div className="space-y-2">
197
210
  {Object.entries(byCategory).map(([category, items]) => (
198
211
  <fieldset key={category}>
199
- <legend className="text-xs font-medium uppercase text-gray-500">{category}</legend>
212
+ <legend className="text-xs font-medium uppercase text-muted-foreground">{category}</legend>
200
213
  <div className="flex flex-wrap gap-x-4 gap-y-1">
201
214
  {items.map((e) => (
202
215
  <label key={e.event} className="flex items-center gap-1 text-sm" title={e.description ?? undefined}>
@@ -218,10 +231,10 @@ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }:
218
231
  </section>
219
232
 
220
233
  {secret ? (
221
- <div role="alert" className="rounded border border-amber-300 bg-amber-50 p-3 text-sm">
234
+ <div role="alert" className="rounded border border-warning-foreground/30 bg-warning p-3 text-sm">
222
235
  <p className="font-medium">Signing secret for {secret.url}. It is shown once.</p>
223
- <code className="mt-1 block break-all rounded bg-white p-2">{secret.value}</code>
224
- <p className="mt-1 text-xs text-gray-600">
236
+ <code className="mt-1 block break-all rounded bg-background p-2">{secret.value}</code>
237
+ <p className="mt-1 text-xs text-muted-foreground">
225
238
  Store it in your server environment as G8_WEBHOOK_SECRET. Never put it in browser code.
226
239
  </p>
227
240
  <Button size="sm" variant="outline" className="mt-2" onClick={() => setSecret(null)}>
@@ -238,13 +251,13 @@ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }:
238
251
  ) : list.length === 0 ? (
239
252
  <EmptyState title="No webhooks yet" description="Create one above to receive events like replies and meetings on your server." />
240
253
  ) : (
241
- <ul className="divide-y rounded border border-gray-200">
254
+ <ul className="divide-y rounded border border-border">
242
255
  {list.map((h) => (
243
256
  <li key={h.id} className="space-y-2 px-3 py-3">
244
257
  <div className="flex flex-wrap items-center justify-between gap-2">
245
258
  <div>
246
259
  {onSelectWebhook ? <button type="button" className="font-medium hover:underline" onClick={() => onSelectWebhook(h)}>{h.name || h.url}</button> : <div className="font-medium">{h.name || h.url}</div>}
247
- <div className="text-xs text-gray-500">
260
+ <div className="text-xs text-muted-foreground">
248
261
  {h.name ? `${h.url} - ` : ""}
249
262
  {(h.events ?? []).length} events - {h.is_active === false ? "paused" : "active"}
250
263
  </div>
@@ -275,12 +288,12 @@ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }:
275
288
  </ul>
276
289
  )}
277
290
 
278
- <details className="rounded border border-gray-200 p-3 text-sm">
291
+ <details className="rounded border border-border p-3 text-sm">
279
292
  <summary className="cursor-pointer font-medium">Verify deliveries (server side)</summary>
280
- <p className="mt-2 text-gray-600">
293
+ <p className="mt-2 text-muted-foreground">
281
294
  Check the signature on the raw body before trusting an event. Deliveries repeat, so deduplicate on the event id.
282
295
  </p>
283
- <pre className="mt-2 overflow-x-auto rounded bg-gray-950 p-3 text-xs text-gray-100">
296
+ <pre className="mt-2 overflow-x-auto rounded bg-muted p-3 text-xs text-foreground">
284
297
  <code>{`const event = g8.webhooks.constructEvent(rawBody,
285
298
  req.headers.get("x-studio-signature") ?? "",
286
299
  req.headers.get("x-studio-timestamp") ?? "",