@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,127 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WorkspaceTools: Slack, Google Workspace and Roam, each as a card with its live status. They connect through a sign-in screen that only runs
5
+ * in the graph8 app, so a tool that is not connected shows a "Connect in graph8" link, never a dead button. Connected tools show what the
6
+ * status reports. Google Workspace also has "Test connection" (a live check) and a Drive browser (folders one level at a time with their
7
+ * files; "Load more" follows Google's page token) for confirming which folders graph8 can read.
8
+ * All reads. Nothing here changes a connection.
9
+ *
10
+ * SDK operations, in order:
11
+ * 1. getSlackStatus / getGoogleWorkspaceStatus / getRoamStatus GET /integrations/slack/status, /integrations/google-workspace, /integrations/roam/status (read)
12
+ * 2. testGoogleWorkspaceConnection POST /integrations/google-workspace/test (read)
13
+ * 3. listGoogleDriveFolders GET /integrations/google-workspace/folders?parent_id&view&page_size&page_token (read)
14
+ * 4. listGoogleDriveFolderFiles GET /integrations/google-workspace/folders/{folder_id}/files?page_size&page_token (read)
15
+ */
16
+ import { useEffect, useState } from "react";
17
+ import { Folder } from "lucide-react";
18
+ import { Button, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { SectionCard } from "@/g8/components/screen/section-card";
21
+ import { StatusPill } from "@/g8/components/screen/status-pill";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
25
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
26
+ import { FactList } from "../lib/facts";
27
+ import { GRAPH8_APP_INTEGRATIONS_PATH, looksConnected } from "../lib/integrations-utils";
28
+
29
+ export interface WorkspaceToolsProps {
30
+ /** Where "Connect" sends people. Default: graph8 Settings > Integrations. */
31
+ connectUrl?: string;
32
+ className?: string;
33
+ }
34
+
35
+ interface DriveFolder { id: string; name: string }
36
+ interface DriveFile { id: string; name: string; mimeType?: string; size?: string | number | null }
37
+ const TOOLS = [
38
+ { key: "slack", label: "Slack", path: "/integrations/slack/status", note: "Send alerts and notifications to Slack channels." },
39
+ { key: "google", label: "Google Workspace", path: "/integrations/google-workspace", note: "Read Drive folders and use your Workspace account." },
40
+ { key: "roam", label: "Roam", path: "/integrations/roam/status", note: "Post to Roam groups and users." },
41
+ ] as const;
42
+ const enc = encodeURIComponent;
43
+ type Query = { data: unknown; error: unknown; loading: boolean; refetch: () => Promise<unknown> };
44
+
45
+ /** One paged Drive list. Folders and files are separate queries, so each keeps its own page token (a token only works for the query that issued it). */
46
+ function usePagedDrive<T>(path: string, key: string, field: "folders" | "files", extraQuery: Record<string, string | number>, enabled: boolean) {
47
+ const [acc, setAcc] = useState<T[]>([]);
48
+ const [token, setToken] = useState<string | undefined>();
49
+ const [next, setNext] = useState<string | undefined>();
50
+ useEffect(() => { setAcc([]); setToken(undefined); setNext(undefined); }, [key]);
51
+ const q = useG8Query((signal) => g8Get(path, { ...extraQuery, page_size: 50, page_token: token }, { signal }), [key, token], { enabled });
52
+ useEffect(() => {
53
+ const d = (q.data as { data?: Record<string, unknown> } | undefined)?.data;
54
+ if (!d || !Array.isArray(d[field])) return;
55
+ setAcc((prev) => (token ? [...prev, ...(d[field] as T[])] : (d[field] as T[])));
56
+ setNext((d.next_page_token as string | null | undefined) ?? undefined);
57
+ }, [q.data]); // eslint-disable-line react-hooks/exhaustive-deps
58
+ return { items: acc, next, loading: q.loading, error: q.error, refetch: q.refetch, more: () => next && setToken(next) };
59
+ }
60
+
61
+ function DriveBrowser() {
62
+ const [trail, setTrail] = useState<DriveFolder[]>([]);
63
+ const parent = trail[trail.length - 1]?.id ?? "root";
64
+ const folders = usePagedDrive<DriveFolder>("/integrations/google-workspace/folders", parent, "folders", { parent_id: parent, view: "my_drive" }, true);
65
+ const files = usePagedDrive<DriveFile>(`/integrations/google-workspace/folders/${enc(parent)}/files`, parent, "files", {}, parent !== "root");
66
+ const go = (t: DriveFolder[]) => setTrail(t);
67
+ return (
68
+ <div className="flex flex-col gap-2">
69
+ <nav aria-label="Folder path" className="flex flex-wrap items-center gap-1 text-sm">
70
+ <button type="button" className="underline" onClick={() => go([])}>My Drive</button>
71
+ {trail.map((f, i) => <span key={f.id}> / <button type="button" className="underline" onClick={() => go(trail.slice(0, i + 1))}>{f.name}</button></span>)}
72
+ </nav>
73
+ {folders.error ? <ApiError error={folders.error} title="Could not list the folders" onRetry={() => void folders.refetch()} /> : null}
74
+ {(folders.loading && folders.items.length === 0) ? <Skeleton className="h-10 w-full" /> : null}
75
+ <ul className="divide-y rounded-md border text-sm" aria-label="Folders">
76
+ {folders.items.map((f) => <li key={f.id}><button type="button" className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent" onClick={() => go([...trail, f])}><Folder className="size-4" aria-hidden />{f.name}</button></li>)}
77
+ </ul>
78
+ {folders.next ? <Button size="sm" variant="outline" className="self-start" loading={folders.loading} onClick={folders.more}>Load more folders</Button> : null}
79
+ {parent !== "root" ? (
80
+ <>
81
+ {files.error ? <ApiError compact error={files.error} title="Could not list the files" onRetry={() => void files.refetch()} /> : null}
82
+ <ul className="divide-y rounded-md border text-sm" aria-label="Files">
83
+ {files.items.map((f) => <li key={f.id} className="flex justify-between px-3 py-1.5"><span>{f.name}</span><span className="text-xs text-muted-foreground">{f.mimeType?.split(".").pop()}</span></li>)}
84
+ </ul>
85
+ {files.next ? <Button size="sm" variant="outline" className="self-start" loading={files.loading} onClick={files.more}>Load more files</Button> : null}
86
+ </>
87
+ ) : null}
88
+ {!folders.loading && !folders.error && folders.items.length === 0 && (parent === "root" || (!files.loading && files.items.length === 0)) ? <p className="text-sm text-muted-foreground">This folder is empty.</p> : null}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ function ToolCard({ tool, connectUrl }: { tool: (typeof TOOLS)[number]; connectUrl: string }) {
94
+ const brand = useBrand();
95
+ const [drive, setDrive] = useState(false);
96
+ const status = useG8Query((signal) => g8Get(tool.path, undefined, { signal }), [tool.path]);
97
+ const test = useG8Mutation(() => g8Post("/integrations/google-workspace/test", {}));
98
+ const connected = looksConnected(status.data);
99
+ const verdict = (test.data as { data?: { success?: boolean; ok?: boolean; message?: string; error?: string } } | undefined)?.data;
100
+ const body = ((status.data as { data?: Record<string, unknown> } | undefined)?.data ?? {}) as Record<string, unknown>;
101
+ const facts = Object.fromEntries(Object.entries(body).filter(([k]) => !["connected", "is_connected"].includes(k)));
102
+ return (
103
+ <SectionCard title={tool.label} description={tool.note} actions={status.data ? <StatusPill tone={connected ? "positive" : "neutral"} dot>{connected ? "Connected" : "Not connected"}</StatusPill> : undefined}>
104
+ {status.error ? <ApiError error={status.error} title={`Could not check ${tool.label}`} onRetry={() => void status.refetch()} /> : null}
105
+ {status.loading && !status.data ? <Skeleton className="h-10 w-full" /> : null}
106
+ {status.data && !connected ? <a className="text-sm underline" href={connectUrl} target="_blank" rel="noreferrer">Connect {tool.label} in {brand.linkLabel}</a> : null}
107
+ {connected ? <FactList data={facts} empty="Connected." /> : null}
108
+ {connected && tool.key === "google" ? (
109
+ <div className="mt-3 flex flex-col gap-2">
110
+ <div className="flex flex-wrap items-center gap-2">
111
+ <Button size="sm" variant="outline" loading={test.loading} onClick={() => void test.mutate(undefined)}>Test connection</Button>
112
+ <Button size="sm" variant="outline" aria-expanded={drive} onClick={() => setDrive((v) => !v)}>{drive ? "Hide Drive" : "Browse Drive"}</Button>
113
+ {test.data !== undefined ? <span role="status" className="text-sm" data-testid="google-test">{verdict?.success === false || verdict?.ok === false ? `The test failed${verdict.message || verdict.error ? `: ${verdict.message ?? verdict.error}` : ""}` : "Google answered: the connection works."}</span> : null}
114
+ </div>
115
+ {test.error ? <ApiError error={test.error} title="The test could not run" /> : null}
116
+ {drive ? <DriveBrowser /> : null}
117
+ </div>
118
+ ) : null}
119
+ </SectionCard>
120
+ );
121
+ }
122
+
123
+ export function WorkspaceTools({ connectUrl: connectUrlProp, className }: WorkspaceToolsProps) {
124
+ const brand = useBrand();
125
+ const connectUrl = connectUrlProp ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
126
+ return <div className={`grid gap-4 md:grid-cols-2 ${className ?? ""}`}>{TOOLS.map((t) => <ToolCard key={t.key} tool={t} connectUrl={connectUrl} />)}</div>;
127
+ }
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ /** Helpers for reading records that come back in a CRM's own shape (HubSpot properties, Salesforce fields, Pipedrive values). */
4
+
5
+ export type CrmRecord = Record<string, unknown> & { id?: string | number; properties?: Record<string, unknown> };
6
+
7
+ const pick = (r: CrmRecord, keys: string[]): string => {
8
+ const sources: Array<Record<string, unknown>> = [r, (r.properties ?? {}) as Record<string, unknown>, ((r as { attributes?: Record<string, unknown> }).attributes ?? {}) as Record<string, unknown>];
9
+ for (const src of sources) for (const k of keys) { const v = src[k]; if (typeof v === "string" && v.trim()) return v; if (typeof v === "number") return String(v); }
10
+ return "";
11
+ };
12
+
13
+ /** A readable name for a CRM record of any kind. */
14
+ export function crmRecordTitle(r: CrmRecord): string {
15
+ const first = pick(r, ["firstname", "first_name", "FirstName"]);
16
+ const last = pick(r, ["lastname", "last_name", "LastName"]);
17
+ return pick(r, ["name", "full_name", "Name", "dealname", "title", "subject"]) || [first, last].filter(Boolean).join(" ") || pick(r, ["email", "Email", "domain", "company"]) || String(r.id ?? "Record");
18
+ }
19
+
20
+ export function crmRecordSubtitle(r: CrmRecord): string {
21
+ return [pick(r, ["email", "Email"]), pick(r, ["company", "Company", "domain"]), pick(r, ["stage", "dealstage", "StageName", "status", "Status"])].filter(Boolean).join(" - ");
22
+ }
23
+
24
+ /** The record's own scalar fields (properties flattened in), for a detail view. Objects and empty values are left out. */
25
+ export function flattenRecord(r: CrmRecord): Record<string, string> {
26
+ const out: Record<string, string> = {};
27
+ const add = (src: Record<string, unknown>) => { for (const [k, v] of Object.entries(src)) if ((typeof v === "string" && v) || typeof v === "number" || typeof v === "boolean") out[k] = String(v); };
28
+ add(r);
29
+ add((r.properties ?? {}) as Record<string, unknown>);
30
+ return out;
31
+ }
32
+
33
+ export const CRM_ENTITIES = [
34
+ { id: "contacts", label: "Contacts" },
35
+ { id: "companies", label: "Companies" },
36
+ { id: "deals", label: "Deals" },
37
+ { id: "leads", label: "Leads" },
38
+ ] as const;
39
+ export type CrmEntity = (typeof CRM_ENTITIES)[number]["id"];
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ /** Renders an open-shaped API object (health, scorecard, status) as labelled facts without guessing its fields. */
4
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
5
+
6
+ const label = (k: string): string => { const t = k.replace(/_/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); };
7
+ const show = (v: unknown): string => (v == null ? "" : typeof v === "string" ? v : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v));
8
+
9
+ export function FactList({ data, empty = "Nothing reported yet." }: { data: unknown; empty?: string }) {
10
+ const entries = Object.entries((data && typeof data === "object" && !Array.isArray(data) ? data : {}) as Record<string, unknown>).filter(([, v]) => v != null && v !== "");
11
+ if (entries.length === 0) return <p className="text-sm text-muted-foreground">{empty}</p>;
12
+ return <KeyValueList columns={2} items={entries.map(([k, v]) => ({ label: label(k), value: show(v), wide: typeof v === "object", mono: typeof v === "object" }))} />;
13
+ }
@@ -7,6 +7,11 @@ import { DEFAULT_BRAND } from "@/g8/hooks/use-brand";
7
7
  export const CRM_PROVIDERS = ["hubspot", "salesforce", "pipedrive", "zoho", "sugarcrm"] as const;
8
8
  export type CrmProvider = (typeof CRM_PROVIDERS)[number];
9
9
 
10
+ /** The connection type the API expects (`CrmConnectionType`): Zoho is `zoho-crm`, the others are their own id. */
11
+ export function crmType(provider: string): string {
12
+ return provider === "zoho" ? "zoho-crm" : provider;
13
+ }
14
+
10
15
  export const PROVIDER_LABELS: Record<CrmProvider, string> = {
11
16
  hubspot: "HubSpot",
12
17
  salesforce: "Salesforce",
@@ -0,0 +1,141 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Paging helpers for the agents block, in the same shape as the foundation's paging primitives (a list never stops silently at a
5
+ * cap: it shows "Load more" while the last page was full).
6
+ *
7
+ * useLoadMoreList offset + limit APIs (the next page starts at items.length).
8
+ * useGrowingList limit-only APIs (a bigger limit refetches the list from the start).
9
+ * LoadMore the footer button plus the error of a failed "more" request.
10
+ */
11
+ import { useCallback, useEffect, useRef, useState } from "react";
12
+ import { Button } from "@graph8/sdk/ui";
13
+ import { ApiError } from "@/g8/components/api-error";
14
+
15
+ export interface LoadMoreList<T> {
16
+ items: T[];
17
+ /** The first page is loading (or reloading after the deps changed). */
18
+ loading: boolean;
19
+ loadingMore: boolean;
20
+ error: unknown;
21
+ moreError: unknown;
22
+ hasMore: boolean;
23
+ /** A limit-only API stopped at its own maximum: there are more rows the API will not return. */
24
+ capped?: boolean;
25
+ loadMore: () => void;
26
+ reload: () => void;
27
+ }
28
+
29
+ interface Options {
30
+ pageSize?: number;
31
+ enabled?: boolean;
32
+ /** useGrowingList: the largest `limit` the API accepts. */
33
+ max?: number;
34
+ }
35
+
36
+ /** Offset paging. `fetchPage(offset, limit)` returns that page's rows; a full page means there may be more. */
37
+ export function useLoadMoreList<T>(
38
+ fetchPage: (offset: number, limit: number, signal: AbortSignal) => Promise<T[]>,
39
+ deps: readonly unknown[],
40
+ { pageSize = 50, enabled = true }: Options = {},
41
+ ): LoadMoreList<T> {
42
+ const [items, setItems] = useState<T[]>([]);
43
+ const [loading, setLoading] = useState(enabled);
44
+ const [loadingMore, setLoadingMore] = useState(false);
45
+ const [error, setError] = useState<unknown>(null);
46
+ const [moreError, setMoreError] = useState<unknown>(null);
47
+ const [hasMore, setHasMore] = useState(false);
48
+ const [tick, setTick] = useState(0);
49
+ const fetchRef = useRef(fetchPage);
50
+ fetchRef.current = fetchPage;
51
+ const gen = useRef(0);
52
+ const count = useRef(0);
53
+ const sig = JSON.stringify([...deps, enabled, pageSize]);
54
+ const lastSig = useRef(sig);
55
+
56
+ useEffect(() => {
57
+ if (!enabled) { setLoading(false); return; }
58
+ const mine = ++gen.current;
59
+ const ctl = new AbortController();
60
+ // A reload() keeps showing the rows it has until the fresh first page lands; a changed filter starts from nothing.
61
+ const fresh = lastSig.current !== sig;
62
+ lastSig.current = sig;
63
+ setLoading(true); setError(null); setMoreError(null);
64
+ if (fresh) { setItems([]); setHasMore(false); count.current = 0; }
65
+ // A reload() refetches as many pages as were showing, so a row opened from page 2 does not vanish; a changed filter starts at page 1.
66
+ const want = fresh ? pageSize : Math.max(count.current, pageSize);
67
+ const load = async (): Promise<T[]> => {
68
+ const all: T[] = [];
69
+ let last = pageSize;
70
+ while (all.length < want && last >= pageSize) {
71
+ const page = await fetchRef.current(all.length, pageSize, ctl.signal);
72
+ all.push(...page);
73
+ last = page.length;
74
+ }
75
+ return all;
76
+ };
77
+ load().then((rows) => {
78
+ if (gen.current !== mine) return;
79
+ setItems(rows); count.current = rows.length; setHasMore(rows.length > 0 && rows.length % pageSize === 0); setLoading(false);
80
+ }, (e) => { if (gen.current === mine) { setError(e); setLoading(false); } });
81
+ return () => ctl.abort();
82
+ }, [...deps, enabled, pageSize, tick]); // eslint-disable-line react-hooks/exhaustive-deps
83
+
84
+ const loadMore = useCallback(() => {
85
+ const mine = gen.current;
86
+ setLoadingMore(true); setMoreError(null);
87
+ fetchRef.current(count.current, pageSize, new AbortController().signal).then((rows) => {
88
+ if (gen.current !== mine) return;
89
+ count.current += rows.length;
90
+ setItems((prev) => [...prev, ...rows]); setHasMore(rows.length >= pageSize); setLoadingMore(false);
91
+ }, (e) => { if (gen.current === mine) { setMoreError(e); setLoadingMore(false); } });
92
+ }, [pageSize]);
93
+
94
+ return { items, loading, loadingMore, error, moreError, hasMore, loadMore, reload: () => setTick((t) => t + 1) };
95
+ }
96
+
97
+ /** Limit-only APIs: asks for `step` rows, and "Load more" asks again for `step` more from the start. */
98
+ export function useGrowingList<T>(
99
+ fetchRows: (limit: number, signal: AbortSignal) => Promise<T[]>,
100
+ deps: readonly unknown[],
101
+ { pageSize = 50, enabled = true, max = Infinity }: Options = {},
102
+ ): LoadMoreList<T> {
103
+ const [limit, setLimit] = useState(pageSize);
104
+ const [rows, setRows] = useState<T[]>([]);
105
+ const [loading, setLoading] = useState(enabled);
106
+ const [error, setError] = useState<unknown>(null);
107
+ const [tick, setTick] = useState(0);
108
+ const [growing, setGrowing] = useState(false);
109
+ const fetchRef = useRef(fetchRows);
110
+ fetchRef.current = fetchRows;
111
+ const depKey = JSON.stringify(deps);
112
+ const lastDeps = useRef(depKey);
113
+
114
+ useEffect(() => { if (lastDeps.current !== depKey) { lastDeps.current = depKey; setLimit(pageSize); } }, [depKey, pageSize]);
115
+ useEffect(() => {
116
+ if (!enabled) { setLoading(false); return; }
117
+ let live = true;
118
+ const ctl = new AbortController();
119
+ setLoading(true); setError(null);
120
+ fetchRef.current(limit, ctl.signal).then((r) => { if (live) { setRows(r); setLoading(false); setGrowing(false); } }, (e) => { if (live) { setError(e); setLoading(false); setGrowing(false); } });
121
+ return () => { live = false; ctl.abort(); };
122
+ }, [limit, depKey, enabled, tick]); // eslint-disable-line react-hooks/exhaustive-deps
123
+
124
+ return {
125
+ items: rows, loading: loading && !growing, loadingMore: loading && growing, error: growing ? null : error, moreError: growing ? error : null,
126
+ hasMore: !loading && rows.length >= limit && limit < max, capped: !loading && rows.length >= limit && limit >= max,
127
+ loadMore: () => { setGrowing(true); setLimit((l) => Math.min(max, l + pageSize)); }, reload: () => setTick((t) => t + 1),
128
+ };
129
+ }
130
+
131
+ /** The footer of a paged list. Renders nothing when everything is shown. */
132
+ export function LoadMore<T>({ list, noun = "rows" }: { list: LoadMoreList<T>; noun?: string }) {
133
+ if (!list.hasMore && !list.moreError && !list.capped) return null;
134
+ return (
135
+ <div className="flex flex-col items-center gap-2 py-2">
136
+ {list.moreError ? <ApiError compact error={list.moreError} title={`Could not load more ${noun}`} onRetry={list.loadMore} /> : null}
137
+ {list.capped ? <p className="text-xs text-muted-foreground">Showing the latest {list.items.length} {noun}. The API returns no more than this.</p> : null}
138
+ {list.hasMore && !list.moreError ? <Button size="sm" variant="outline" loading={list.loadingMore} onClick={list.loadMore}>Load more {noun}</Button> : null}
139
+ </div>
140
+ );
141
+ }
@@ -36,7 +36,7 @@ function Coverage({ account }: { account: AbmAccount }) {
36
36
  <li
37
37
  key={category}
38
38
  title={`${cov.contact_count ?? 0} contacts, ${cov.signal_count ?? 0} signals`}
39
- className="rounded border border-gray-200 px-1.5 py-0.5 text-xs text-gray-700"
39
+ className="rounded border border-border px-1.5 py-0.5 text-xs text-foreground"
40
40
  >
41
41
  {category}: {cov.barometer ?? "none"}
42
42
  </li>
@@ -52,9 +52,9 @@ function Badge({ account, compact }: { account: AbmAccount; compact?: boolean })
52
52
  <div className="flex items-center gap-2">
53
53
  <span className={`rounded px-2 py-0.5 text-xs font-medium ${strengthClass(account.account_strength)}`}>{label}</span>
54
54
  {account.is_high_intent ? (
55
- <span className="rounded bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800">High intent</span>
55
+ <span className="rounded bg-error px-2 py-0.5 text-xs font-medium text-error-foreground">High intent</span>
56
56
  ) : null}
57
- <span className="text-xs text-gray-500">{account.total_signal_count ?? 0} signals</span>
57
+ <span className="text-xs text-muted-foreground">{account.total_signal_count ?? 0} signals</span>
58
58
  </div>
59
59
  {compact ? null : <Coverage account={account} />}
60
60
  </div>
@@ -69,7 +69,7 @@ export function AccountIntentScore({ companyIds, days = 30, compact, className }
69
69
  const status = (error as { status?: number }).status;
70
70
  const text = status === 403 ? "Intent scoring is not enabled for this workspace." : "Could not load intent.";
71
71
  return (
72
- <span role="alert" className={`text-xs text-red-700 ${className ?? ""}`}>
72
+ <span role="alert" className={`text-xs text-error-foreground ${className ?? ""}`}>
73
73
  {text}
74
74
  </span>
75
75
  );
@@ -81,7 +81,7 @@ export function AccountIntentScore({ companyIds, days = 30, compact, className }
81
81
  return account ? (
82
82
  <Badge key={id} account={{ ...account, company_id: id }} compact={compact} />
83
83
  ) : (
84
- <span key={id} className="block text-xs text-gray-500">
84
+ <span key={id} className="block text-xs text-muted-foreground">
85
85
  No signal
86
86
  </span>
87
87
  );
@@ -59,7 +59,7 @@ function UrlLookup() {
59
59
  { enabled: submitted != null },
60
60
  );
61
61
  return (
62
- <section aria-label="URL lookup" className="rounded border border-gray-200 p-3">
62
+ <section aria-label="URL lookup" className="rounded border border-border p-3">
63
63
  <form
64
64
  className="flex gap-2"
65
65
  onSubmit={(e) => {
@@ -76,13 +76,13 @@ function UrlLookup() {
76
76
  {result.error ? <ApiError error={result.error} onRetry={() => void result.refetch()} /> : null}
77
77
  {submitted && !result.loading && !result.error ? (
78
78
  (result.data ?? []).length === 0 ? (
79
- <p className="mt-3 text-sm text-gray-500">No identified companies visited this URL in the last 30 days.</p>
79
+ <p className="mt-3 text-sm text-muted-foreground">No identified companies visited this URL in the last 30 days.</p>
80
80
  ) : (
81
81
  <ul className="mt-3 divide-y text-sm">
82
82
  {(result.data ?? []).map((c) => (
83
83
  <li key={`${c.company_id}-${c.domain}`} className="flex justify-between py-1.5">
84
84
  <span>{c.name ?? c.domain}</span>
85
- <span className="text-gray-500">
85
+ <span className="text-muted-foreground">
86
86
  {c.visit_count} visits, {c.contacts_seen} people
87
87
  </span>
88
88
  </li>
@@ -132,7 +132,7 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
132
132
  </div>
133
133
 
134
134
  {feed.error && status === 403 ? (
135
- <p role="alert" className="rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900">
135
+ <p role="alert" className="rounded border border-warning-foreground/25 bg-warning p-3 text-sm text-warning-foreground">
136
136
  Account intent is not enabled for this workspace. Ask {brand.linkLabel} to switch on ABM intent.
137
137
  </p>
138
138
  ) : feed.error ? (
@@ -167,16 +167,16 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
167
167
  >
168
168
  <TableCell>
169
169
  <div className="font-medium">{a.company_name ?? a.company_domain ?? "Unknown company"}</div>
170
- <div className="text-xs text-gray-500">{a.company_domain}</div>
170
+ <div className="text-xs text-muted-foreground">{a.company_domain}</div>
171
171
  </TableCell>
172
172
  <TableCell>
173
173
  <span className={`rounded px-2 py-0.5 text-xs ${strengthClass(a.account_strength)}`}>{a.account_strength ?? "-"}</span>
174
- {a.is_high_intent ? <span className="ml-1 text-xs font-medium text-red-700">High intent</span> : null}
174
+ {a.is_high_intent ? <span className="ml-1 text-xs font-medium text-error-foreground">High intent</span> : null}
175
175
  </TableCell>
176
176
  <TableCell>
177
177
  {a.total_signal_count ?? 0}
178
178
  {typeof a.trend_pct === "number" ? (
179
- <span className="ml-1 text-xs text-gray-500">
179
+ <span className="ml-1 text-xs text-muted-foreground">
180
180
  ({a.trend_pct > 0 ? "+" : ""}
181
181
  {Math.round(a.trend_pct)}%)
182
182
  </span>
@@ -195,7 +195,7 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
195
195
  </Table>
196
196
  )}
197
197
 
198
- <div className="flex items-center justify-between text-sm text-gray-600">
198
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
199
199
  <span>{total != null ? `${total.toLocaleString()} accounts` : ""}</span>
200
200
  <div className="flex gap-2">
201
201
  <Button variant="outline" size="sm" disabled={page <= 1 || feed.loading} onClick={() => setPage((p) => p - 1)}>
@@ -185,8 +185,8 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
185
185
  return (
186
186
  <div className={`space-y-4 ${className ?? ""}`}>
187
187
  <div className="grid gap-3 md:grid-cols-2">
188
- <section className="space-y-2 rounded border border-gray-200 p-3">
189
- <h3 className="text-sm font-semibold">Track keywords</h3>
188
+ <section className="space-y-2 rounded border border-border p-3">
189
+ <h3 className="text-sm font-medium">Track keywords</h3>
190
190
  <Textarea
191
191
  aria-label="Keywords"
192
192
  rows={3}
@@ -198,8 +198,8 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
198
198
  Track keywords
199
199
  </Button>
200
200
  </section>
201
- <section className="space-y-2 rounded border border-gray-200 p-3">
202
- <h3 className="text-sm font-semibold">Track a competitor domain</h3>
201
+ <section className="space-y-2 rounded border border-border p-3">
202
+ <h3 className="text-sm font-medium">Track a competitor domain</h3>
203
203
  <Input
204
204
  aria-label="Competitor domain"
205
205
  placeholder="competitor.com"
@@ -228,7 +228,7 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
228
228
  </Button>
229
229
  </div>
230
230
  {domainResult ? (
231
- <p className="text-xs text-gray-600" role="status">
231
+ <p className="text-xs text-muted-foreground" role="status">
232
232
  {domainResult.dry_run ? "Preview: " : "Saved: "}
233
233
  {domainResult.pages_seeded ?? 0} pages, {domainResult.contacts_seeded ?? 0} people, {domainResult.companies_seeded ?? 0}{" "}
234
234
  companies. {domainResult.message}
@@ -301,10 +301,10 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
301
301
  )}
302
302
 
303
303
  {companies ? (
304
- <section className="rounded border border-gray-200 p-3" aria-label="Keyword companies">
305
- <h3 className="text-sm font-semibold">Companies interested in "{companies.keyword}"</h3>
304
+ <section className="rounded border border-border p-3" aria-label="Keyword companies">
305
+ <h3 className="text-sm font-medium">Companies interested in "{companies.keyword}"</h3>
306
306
  {companies.rows.length === 0 ? (
307
- <p className="mt-2 text-sm text-gray-500">No resolved companies yet.</p>
307
+ <p className="mt-2 text-sm text-muted-foreground">No resolved companies yet.</p>
308
308
  ) : (
309
309
  <ul className="mt-2 grid gap-1 text-sm md:grid-cols-2">
310
310
  {companies.rows.map((c, i) => (
@@ -88,8 +88,8 @@ export function formatDate(value: string | null | undefined): string {
88
88
  /** Tailwind classes for an account-strength label. Unknown labels stay neutral. */
89
89
  export function strengthClass(strength: string | null | undefined): string {
90
90
  const s = (strength ?? "").toLowerCase();
91
- if (s.includes("strong") || s === "high") return "bg-green-100 text-green-800";
92
- if (s.includes("moderate") || s === "medium") return "bg-amber-100 text-amber-800";
93
- if (s.includes("weak") || s === "low") return "bg-gray-100 text-gray-700";
94
- return "bg-gray-100 text-gray-700";
91
+ if (s.includes("strong") || s === "high") return "bg-positive text-positive-foreground";
92
+ if (s.includes("moderate") || s === "medium") return "bg-warning text-warning-foreground";
93
+ if (s.includes("weak") || s === "low") return "bg-muted text-foreground";
94
+ return "bg-muted text-foreground";
95
95
  }
@@ -13,6 +13,7 @@ import { useState } from "react";
13
13
  import { Radar } from "lucide-react";
14
14
  import { Button, Skeleton } from "@graph8/sdk/ui";
15
15
  import { ApiError } from "@/g8/components/api-error";
16
+ import { FirstNotice } from "@/g8/components/paging-notices";
16
17
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
17
18
  import { EmptyState } from "@/g8/components/empty-state";
18
19
  import { SectionCard } from "@/g8/components/screen/section-card";
@@ -42,8 +43,11 @@ export function AccountBriefTab({ companyId, name }: { companyId: number; name?:
42
43
  );
43
44
  }
44
45
 
46
+ /** The signals API has no paging: a full read is announced under the list. */
47
+ const SIGNAL_LIMIT = 20;
48
+
45
49
  export function AccountSignalsTab({ companyId }: { companyId: number }) {
46
- const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: 20 }, { signal }), [companyId]);
50
+ const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: SIGNAL_LIMIT }, { signal }), [companyId]);
47
51
  if (q.error) return <ApiError error={q.error} title="Could not load signals" onRetry={() => void q.refetch()} />;
48
52
  if (q.loading && !q.data) return <Skeleton className="h-24 w-full" aria-label="Loading signals" />;
49
53
  const payload = body(q.data);
@@ -51,6 +55,7 @@ export function AccountSignalsTab({ companyId }: { companyId: number }) {
51
55
  const signals = raw.map(asRec);
52
56
  if (signals.length === 0) return <EmptyState icon={<Radar className="size-8" />} title="Quiet" description="No recent buying or activity signals for this account." />;
53
57
  return (
58
+ <>
54
59
  <ul className="divide-y rounded-md border" aria-label="Signals">
55
60
  {signals.map((s, i) => {
56
61
  const when = text(s.detected_at ?? s.created_at ?? s.occurred_at);
@@ -62,5 +67,7 @@ export function AccountSignalsTab({ companyId }: { companyId: number }) {
62
67
  );
63
68
  })}
64
69
  </ul>
70
+ <FirstNotice shown={signals.length} limit={SIGNAL_LIMIT} noun="signals" />
71
+ </>
65
72
  );
66
73
  }
@@ -35,15 +35,18 @@ export interface ChurnListProps {
35
35
  }
36
36
 
37
37
  /** Loaded on click: one call per row up front would be 25 calls for a page the person may only skim. */
38
+ /** The signals API has no paging: a full read is shown as "20+ recent signals". */
39
+ const SIGNAL_LIMIT = 20;
40
+
38
41
  function Intent({ companyId }: { companyId: number }) {
39
42
  const [asked, setAsked] = useState(false);
40
- const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: 20 }, { signal }), [companyId], { enabled: asked });
43
+ const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: SIGNAL_LIMIT }, { signal }), [companyId], { enabled: asked });
41
44
  if (!asked) return <Button size="sm" variant="ghost" aria-label={`Check activity for ${companyId}`} onClick={() => setAsked(true)}>Check</Button>;
42
45
  if (q.loading) return <Skeleton className="h-4 w-16" />;
43
46
  if (q.error) return <span className="text-muted-foreground text-xs">n/a</span>;
44
47
  const payload = body(q.data);
45
48
  const signals = Array.isArray(payload) ? payload : Array.isArray(asRec(payload).signals) ? (asRec(payload).signals as unknown[]) : [];
46
- return <span className="text-xs">{signals.length ? `${signals.length} recent signals` : "Quiet"}</span>;
49
+ return <span className="text-xs">{signals.length ? `${signals.length}${signals.length >= SIGNAL_LIMIT ? "+" : ""} recent signals` : "Quiet"}</span>;
47
50
  }
48
51
 
49
52
  export function ChurnList({ stageSlug = "churned", winBackSlug = "leads", onOpenCompany, pageSize = 25, className }: ChurnListProps) {
@@ -41,7 +41,7 @@ function Summary() {
41
41
  return (
42
42
  <div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4" data-testid="revenue-summary">
43
43
  {tiles.map(([key, value]) => (
44
- <Card key={key}><CardContent className="p-4"><div className="text-2xl font-semibold">{value.toLocaleString()}</div><div className="text-muted-foreground text-xs capitalize">{key.replace(/_count$/, "").replace(/_/g, " ")}</div></CardContent></Card>
44
+ <Card key={key}><CardContent className="p-4"><div className="text-2xl font-medium">{value.toLocaleString()}</div><div className="text-muted-foreground text-xs capitalize">{key.replace(/_count$/, "").replace(/_/g, " ")}</div></CardContent></Card>
45
45
  ))}
46
46
  </div>
47
47
  );
@@ -39,7 +39,7 @@ and `contacts:read/write`. The screens use the foundation screen kit (`component
39
39
  shows `ready_count / warned / blocked` and retries with `conflict_resolution: "skip_all" | "add_all"`.
40
40
  4. **Members**: `ContactTable listId={id}` (or `GET /contacts?list_id=`). Removing uses
41
41
  `DELETE /lists/{list_id}/contacts { contact_ids }`; contacts are never deleted by it.
42
- 5. **Suppress**: `POST /contacts/suppressions/bulk-add { contact_ids, channel, reason }`; read back with
42
+ 5. **Suppress**: pick people by name with the `ContactPicker` (pickers block) (no ids to type; pasting ids stays under "Paste ids instead"), then `POST /contacts/suppressions/bulk-add { contact_ids, channel, reason }`; read back with
43
43
  `GET /contacts/suppressions`. The ledger has ids only (no names or emails): open the contact for details.
44
44
  6. **Reinstate**: `POST /contacts/suppressions/reinstate { contact_ids }`. Contacts who unsubscribed themselves
45
45
  can be refused; the answer lists them in `skipped`.
@@ -107,3 +107,9 @@ Ask the person: who may reinstate suppressed contacts (decide it in the proxy's
107
107
  | checkContactSuppression | GET /contacts/{contact_id}/suppression | read | SuppressionManager |
108
108
  | reinstateSuppressions | POST /contacts/suppressions/reinstate | write | SuppressionManager |
109
109
  | refreshDynamicList, checkListIntegrity | POST /lists/{id}/refresh, /lists/{id}/integrity-check | write, read | recipes |
110
+
111
+ ## Cross-block glue
112
+
113
+ - `AddCompanyPeopleToListButton` (`components/add-company-people-to-list-button.tsx`): "Add people to list" for companies. Lists hold contacts, so it reads the people at the companies (`GET /contacts?company_id=`, up to 500) and opens `AddToListButton` for them. `AddToListButton` gains optional `defaultOpen`, `hideTrigger` and `onClosed`; `ListsScreen` gains optional `onOpenContact` so list members open in your contacts screen.
114
+
115
+ - `RecordListsTab` (`components/record-lists-tab.tsx`): the lists one contact belongs to (`getContactLists`), with `AddToListButton` and a confirmed Remove (`removeContactsFromList`, destructive). `extraDetailTabs` entry for ContactsScreen: `{ id: "lists", label: "Lists", render: (c) => <RecordListsTab contactId={c.id} onOpenList={(id) => router.push(`/lists?id=${id}`)} /> }`.