@graph8/sdk 0.362.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -23,14 +23,21 @@ 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";
29
32
  import { arrayOf, formatWhen } from "../lib/integrations-utils";
33
+ import { plural } from "@/g8/lib/plural";
34
+ import { DEFAULT_BRAND, useBrand } from "@/g8/hooks/use-brand";
30
35
 
31
36
  export interface WebhookManagerProps {
32
37
  /** Pre-fill the endpoint URL of the create form (your /api/webhooks/graph8 route). */
33
38
  defaultUrl?: string;
39
+ /** Called when a person opens a webhook (show a WebhookDetail). */
40
+ onSelectWebhook?: (webhook: { id: string; url: string; name?: string | null }) => void;
34
41
  className?: string;
35
42
  }
36
43
 
@@ -65,13 +72,13 @@ type Pending =
65
72
  | { kind: "rotate"; hook: Webhook }
66
73
  | { kind: "delete"; hook: Webhook };
67
74
 
68
- function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: string; label: string } {
75
+ function copyFor(p: Pending, platform: string = DEFAULT_BRAND.linkLabel): { tier: ConfirmTier; title: string; description: string; label: string } {
69
76
  switch (p.kind) {
70
77
  case "create":
71
78
  return {
72
79
  tier: "external",
73
- title: `Send ${p.events.length} event type${p.events.length === 1 ? "" : "s"} to ${p.url}`,
74
- description: "graph8 will POST signed events to this URL as they happen.",
80
+ title: `Send ${plural(p.events.length, "event type")} to ${p.url}`,
81
+ description: `${platform} will POST signed events to this URL as they happen.`,
75
82
  label: "Create webhook",
76
83
  };
77
84
  case "toggle":
@@ -80,7 +87,7 @@ function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: s
80
87
  title: `${p.hook.is_active === false ? "Resume" : "Pause"} deliveries to ${p.hook.url}`,
81
88
  description:
82
89
  p.hook.is_active === false
83
- ? "graph8 starts sending events to this URL again."
90
+ ? `${platform} starts sending events to this URL again.`
84
91
  : "Events stop being sent to this URL until you resume.",
85
92
  label: p.hook.is_active === false ? "Resume" : "Pause",
86
93
  };
@@ -101,16 +108,24 @@ function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: s
101
108
  }
102
109
  }
103
110
 
111
+ /** Deliveries per read; "Load more" appends the next page. */
112
+ const DELIVERY_PAGE = 20;
113
+
104
114
  function Deliveries({ hook }: { hook: Webhook }) {
105
- const list = useG8Query((signal) => g8Get(`/webhooks/${hook.id}/deliveries`, { page: 1, limit: 20 }, { signal }), [hook.id]);
106
- 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" />;
107
121
  if (list.error) return <ApiError error={list.error} onRetry={() => void list.refetch()} />;
108
- const rows = arrayOf<Delivery>(list.data);
109
- 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>;
110
124
  return (
125
+ <>
111
126
  <table className="w-full text-sm" aria-label="Deliveries">
112
127
  <thead>
113
- <tr className="text-left text-gray-500">
128
+ <tr className="text-left text-muted-foreground">
114
129
  <th>Event</th>
115
130
  <th>Status</th>
116
131
  <th>Code</th>
@@ -130,10 +145,13 @@ function Deliveries({ hook }: { hook: Webhook }) {
130
145
  ))}
131
146
  </tbody>
132
147
  </table>
148
+ <LoadMore shown={rows.length} noun="deliveries" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
149
+ </>
133
150
  );
134
151
  }
135
152
 
136
- export function WebhookManager({ defaultUrl = "", className }: WebhookManagerProps) {
153
+ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }: WebhookManagerProps) {
154
+ const brand = useBrand();
137
155
  const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
138
156
  const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
139
157
  const [url, setUrl] = useState(defaultUrl);
@@ -169,29 +187,29 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
169
187
  );
170
188
  const urlOk = /^https:\/\/\S+$/.test(url.trim());
171
189
  const toggle = (event: string) => setChosen((c) => (c.includes(event) ? c.filter((x) => x !== event) : [...c, event]));
172
- const confirm = pending ? copyFor(pending) : null;
190
+ const confirm = pending ? copyFor(pending, brand.linkLabel) : null;
173
191
 
174
192
  return (
175
193
  <div className={`space-y-4 ${className ?? ""}`}>
176
- <section className="space-y-3 rounded border border-gray-200 p-3" aria-label="New webhook">
177
- <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>
178
196
  <div className="grid gap-2 md:grid-cols-2">
179
197
  <Input
180
198
  aria-label="Endpoint URL"
181
- placeholder="https://yourapp.com/api/webhooks/graph8"
199
+ placeholder={`https://yourapp.com/api/webhooks/${brand.linkLabel.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`}
182
200
  value={url}
183
201
  onChange={(e) => setUrl(e.target.value)}
184
202
  />
185
203
  <Input aria-label="Name" placeholder="Name (optional)" value={name} onChange={(e) => setName(e.target.value)} />
186
204
  </div>
187
- {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}
188
206
  {events.loading ? (
189
207
  <Skeleton className="h-12 w-full" />
190
208
  ) : (
191
209
  <div className="space-y-2">
192
210
  {Object.entries(byCategory).map(([category, items]) => (
193
211
  <fieldset key={category}>
194
- <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>
195
213
  <div className="flex flex-wrap gap-x-4 gap-y-1">
196
214
  {items.map((e) => (
197
215
  <label key={e.event} className="flex items-center gap-1 text-sm" title={e.description ?? undefined}>
@@ -213,10 +231,10 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
213
231
  </section>
214
232
 
215
233
  {secret ? (
216
- <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">
217
235
  <p className="font-medium">Signing secret for {secret.url}. It is shown once.</p>
218
- <code className="mt-1 block break-all rounded bg-white p-2">{secret.value}</code>
219
- <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">
220
238
  Store it in your server environment as G8_WEBHOOK_SECRET. Never put it in browser code.
221
239
  </p>
222
240
  <Button size="sm" variant="outline" className="mt-2" onClick={() => setSecret(null)}>
@@ -233,13 +251,13 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
233
251
  ) : list.length === 0 ? (
234
252
  <EmptyState title="No webhooks yet" description="Create one above to receive events like replies and meetings on your server." />
235
253
  ) : (
236
- <ul className="divide-y rounded border border-gray-200">
254
+ <ul className="divide-y rounded border border-border">
237
255
  {list.map((h) => (
238
256
  <li key={h.id} className="space-y-2 px-3 py-3">
239
257
  <div className="flex flex-wrap items-center justify-between gap-2">
240
258
  <div>
241
- <div className="font-medium">{h.name || h.url}</div>
242
- <div className="text-xs text-gray-500">
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>}
260
+ <div className="text-xs text-muted-foreground">
243
261
  {h.name ? `${h.url} - ` : ""}
244
262
  {(h.events ?? []).length} events - {h.is_active === false ? "paused" : "active"}
245
263
  </div>
@@ -270,12 +288,12 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
270
288
  </ul>
271
289
  )}
272
290
 
273
- <details className="rounded border border-gray-200 p-3 text-sm">
291
+ <details className="rounded border border-border p-3 text-sm">
274
292
  <summary className="cursor-pointer font-medium">Verify deliveries (server side)</summary>
275
- <p className="mt-2 text-gray-600">
293
+ <p className="mt-2 text-muted-foreground">
276
294
  Check the signature on the raw body before trusting an event. Deliveries repeat, so deduplicate on the event id.
277
295
  </p>
278
- <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">
279
297
  <code>{`const event = g8.webhooks.constructEvent(rawBody,
280
298
  req.headers.get("x-studio-signature") ?? "",
281
299
  req.headers.get("x-studio-timestamp") ?? "",
@@ -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
+ }
@@ -2,9 +2,16 @@
2
2
 
3
3
  /** Types and helpers shared by the integrations components. */
4
4
 
5
+ import { DEFAULT_BRAND } from "@/g8/hooks/use-brand";
6
+
5
7
  export const CRM_PROVIDERS = ["hubspot", "salesforce", "pipedrive", "zoho", "sugarcrm"] as const;
6
8
  export type CrmProvider = (typeof CRM_PROVIDERS)[number];
7
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
+
8
15
  export const PROVIDER_LABELS: Record<CrmProvider, string> = {
9
16
  hubspot: "HubSpot",
10
17
  salesforce: "Salesforce",
@@ -14,7 +21,10 @@ export const PROVIDER_LABELS: Record<CrmProvider, string> = {
14
21
  };
15
22
 
16
23
  /** Where the person completes an OAuth connection. The API has no way to run the consent screen for you. */
17
- export const GRAPH8_APP_INTEGRATIONS_URL = "https://app.graph8.com/studio/settings?tab=integrations";
24
+ /** The path on the platform's web app; components resolve it against the brand (`brandUrl(useBrand(), ...)`). */
25
+ export const GRAPH8_APP_INTEGRATIONS_PATH = "/studio/settings?tab=integrations";
26
+ /** The default (graph8) URL. Kept for existing imports; prefer GRAPH8_APP_INTEGRATIONS_PATH with useBrand. */
27
+ export const GRAPH8_APP_INTEGRATIONS_URL = `${DEFAULT_BRAND.appUrl}${GRAPH8_APP_INTEGRATIONS_PATH}`;
18
28
 
19
29
  export interface CrmSyncRow {
20
30
  provider: string;
@@ -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
+ }
@@ -5,6 +5,20 @@ and score any account by strength and buying-committee coverage. The Apollo / 6s
5
5
 
6
6
  Depends on: `crm` (account rows link to your company records). Install: `npx @graph8/sdk add intent`.
7
7
 
8
+ ## Screens
9
+
10
+ `IntentScreen` (`components/intent-screen.tsx`) is the finished page: copy it as your route. No props are required.
11
+
12
+ - **Accounts tab**: the intent feed with a 7/30/90 day window, a high-intent filter, a keyword scope chip (`?kw=<keyword id>`),
13
+ a stat strip, and a detail panel for the clicked account (signals, trend, top keywords, buying-committee coverage). Deep link:
14
+ `?id=<company id>`; an account with no signal in the window shows "No intent for this account".
15
+ "Who visited this URL" lives under the table.
16
+ - **Keywords tab**: track keywords, preview then track a competitor domain, load the companies behind a keyword, stop tracking.
17
+ Clicking a keyword opens its detail; "Show accounts" there scopes the Accounts tab to it. Every billable or destructive
18
+ call is confirmed.
19
+ - Slots: `toolbarActions`, `renderDetailActions(account)`, `extraDetailTabs`.
20
+ - Prerequisites: ABM intent enabled for the workspace (403 is explained inline) and something to measure.
21
+
8
22
  ## Components
9
23
 
10
24
  | Component | What it does | Tiers |
@@ -61,6 +61,31 @@
61
61
  "tiers": [
62
62
  "read"
63
63
  ]
64
+ },
65
+ {
66
+ "name": "IntentScreen",
67
+ "file": "components/intent-screen.tsx",
68
+ "purpose": "Finished buyer-intent page: accounts feed with window and keyword filters, account detail panel (signals, top keywords, buying committee) and keyword/competitor tracking.",
69
+ "operations": [
70
+ "list_abm_companies_intent_abm_companies_get"
71
+ ],
72
+ "tiers": [
73
+ "read"
74
+ ]
75
+ },
76
+ {
77
+ "name": "KeywordDetail",
78
+ "file": "components/keyword-detail.tsx",
79
+ "purpose": "Complete screen for one tracked keyword: fields, accounts showing intent, companies behind it, stop tracking.",
80
+ "operations": [
81
+ "keywords_list_intent_keywords_list_post",
82
+ "keyword_companies_intent_keywords__keyword_id__companies_post",
83
+ "keywords_delete_intent_keywords__keyword_id__delete"
84
+ ],
85
+ "tiers": [
86
+ "billable",
87
+ "destructive"
88
+ ]
64
89
  }
65
90
  ],
66
91
  "hooks": [
@@ -133,6 +158,7 @@
133
158
  }
134
159
  ],
135
160
  "peerDependencies": {
136
- "react": ">=18"
161
+ "react": ">=18",
162
+ "lucide-react": ">=0.400"
137
163
  }
138
164
  }
@@ -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
  );