@graph8/sdk 0.361.0 → 0.370.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -2,6 +2,8 @@
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
 
@@ -14,7 +16,10 @@ export const PROVIDER_LABELS: Record<CrmProvider, string> = {
14
16
  };
15
17
 
16
18
  /** 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";
19
+ /** The path on the platform's web app; components resolve it against the brand (`brandUrl(useBrand(), ...)`). */
20
+ export const GRAPH8_APP_INTEGRATIONS_PATH = "/studio/settings?tab=integrations";
21
+ /** The default (graph8) URL. Kept for existing imports; prefer GRAPH8_APP_INTEGRATIONS_PATH with useBrand. */
22
+ export const GRAPH8_APP_INTEGRATIONS_URL = `${DEFAULT_BRAND.appUrl}${GRAPH8_APP_INTEGRATIONS_PATH}`;
18
23
 
19
24
  export interface CrmSyncRow {
20
25
  provider: string;
@@ -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
  }
@@ -20,6 +20,8 @@ import { ApiError } from "@/g8/components/api-error";
20
20
  import { EmptyState } from "@/g8/components/empty-state";
21
21
  import { formatDate, rowsOf, strengthClass, totalOf } from "../lib/intent-utils";
22
22
  import type { AbmAccount } from "../lib/intent-utils";
23
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
24
+ import { useBrand } from "@/g8/hooks/use-brand";
23
25
 
24
26
  export interface IntentFeedProps {
25
27
  /** Look-back window in days. Default 30. */
@@ -93,6 +95,7 @@ function UrlLookup() {
93
95
  }
94
96
 
95
97
  export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSeconds, onSelectAccount, className }: IntentFeedProps) {
98
+ const brand = useBrand();
96
99
  const [highOnly, setHighOnly] = useState(false);
97
100
  const keywordKey = (keywordIds ?? []).join(",");
98
101
  // The page belongs to one set of inputs: when days, keywords or page size change, start again at page 1.
@@ -130,7 +133,7 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
130
133
 
131
134
  {feed.error && status === 403 ? (
132
135
  <p role="alert" className="rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900">
133
- Account intent is not enabled for this workspace. Ask graph8 to switch on ABM intent.
136
+ Account intent is not enabled for this workspace. Ask {brand.linkLabel} to switch on ABM intent.
134
137
  </p>
135
138
  ) : feed.error ? (
136
139
  <ApiError error={feed.error} onRetry={() => void feed.refetch()} />
@@ -160,7 +163,7 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
160
163
  <TableRow
161
164
  key={String(a.global_company_id ?? a.company_id ?? a.company_domain)}
162
165
  className={onSelectAccount ? "cursor-pointer" : undefined}
163
- onClick={() => onSelectAccount?.(a)}
166
+ {...(onSelectAccount ? rowActivationProps(() => onSelectAccount(a)) : {})}
164
167
  >
165
168
  <TableCell>
166
169
  <div className="font-medium">{a.company_name ?? a.company_domain ?? "Unknown company"}</div>
@@ -28,12 +28,15 @@ import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
28
28
  import { EmptyState } from "@/g8/components/empty-state";
29
29
  import { rowsOf } from "../lib/intent-utils";
30
30
  import type { IntentKeywordRow } from "../lib/intent-utils";
31
+ import { plural } from "@/g8/lib/plural";
31
32
 
32
33
  export interface IntentKeywordManagerProps {
33
34
  /** Ask before listing (the list is billable in the contract). Default true. */
34
35
  confirmListing?: boolean;
35
36
  pageSize?: number;
36
37
  onSelectKeyword?: (keyword: IntentKeywordRow) => void;
38
+ /** Keyword ids to hide (for example one that was just removed from its detail screen). */
39
+ hiddenIds?: readonly string[];
37
40
  className?: string;
38
41
  }
39
42
 
@@ -76,7 +79,7 @@ function describe(p: Pending): { tier: ConfirmTier; title: string; description:
76
79
  case "add":
77
80
  return {
78
81
  tier: "billable",
79
- title: `Track ${p.keywords.length} keyword${p.keywords.length === 1 ? "" : "s"}`,
82
+ title: `Track ${plural(p.keywords.length, "keyword")}`,
80
83
  description: p.keywords.join(", "),
81
84
  confirmLabel: "Track keywords",
82
85
  };
@@ -114,7 +117,7 @@ function describe(p: Pending): { tier: ConfirmTier; title: string; description:
114
117
  const companyName = (c: CompanyRow) =>
115
118
  c.name ?? c.company_name ?? c.COMPANY_NAME ?? c.domain ?? c.company_domain ?? c.COMPANY_DOMAIN ?? "Unknown";
116
119
 
117
- export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onSelectKeyword, className }: IntentKeywordManagerProps) {
120
+ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onSelectKeyword, hiddenIds, className }: IntentKeywordManagerProps) {
118
121
  const [rows, setRows] = useState<IntentKeywordRow[] | null>(null);
119
122
  const [pending, setPending] = useState<Pending | null>(null);
120
123
  const [keywordText, setKeywordText] = useState("");
@@ -172,6 +175,7 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
172
175
  // Load once on mount when listing needs no confirmation.
173
176
  }, [confirmListing]); // eslint-disable-line react-hooks/exhaustive-deps
174
177
 
178
+ const visibleRows = rows == null ? null : rows.filter((k) => !(hiddenIds ?? []).includes(k.id));
175
179
  const confirm = pending ? describe(pending) : null;
176
180
  const words = keywordText
177
181
  .split(/[\n,]/)
@@ -245,7 +249,7 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
245
249
  action={<Button onClick={() => setPending({ kind: "list" })}>Load keywords</Button>}
246
250
  />
247
251
  )
248
- ) : rows.length === 0 ? (
252
+ ) : (visibleRows ?? []).length === 0 ? (
249
253
  <EmptyState
250
254
  title="No tracked keywords yet"
251
255
  description="Add a keyword or a competitor domain above. Accounts that visit the matching pages show up in the intent feed."
@@ -267,7 +271,7 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
267
271
  </TableRow>
268
272
  </TableHeader>
269
273
  <TableBody>
270
- {rows.map((k) => (
274
+ {(visibleRows ?? []).map((k) => (
271
275
  <TableRow key={k.id}>
272
276
  <TableCell>
273
277
  <button type="button" className="font-medium underline-offset-2 hover:underline" onClick={() => onSelectKeyword?.(k)}>
@@ -0,0 +1,265 @@
1
+ "use client";
2
+
3
+ /**
4
+ * IntentScreen: the finished buyer-intent product page (Apollo / 6sense "intent"). Two tabs:
5
+ * Accounts accounts showing intent, strongest first (IntentFeed), a look-back window filter, a keyword scope chip,
6
+ * and the selected account's detail panel (signals, trend, top keywords, buying-committee coverage).
7
+ * Keywords track keywords and competitor domains (IntentKeywordManager). Each keyword opens its complete detail screen
8
+ * (KeywordDetail: fields, accounts showing intent for it, companies behind it, stop tracking; `?id=`).
9
+ * Every detail also opens as a full page with `?view=full`.
10
+ *
11
+ * User flows: see who is showing intent, filter to high intent, change the window, open an account (deep link
12
+ * `?id=<company id>`), read its buying committee, look up who visited a URL, track keywords, preview and track a
13
+ * competitor domain, list the companies behind a keyword, stop tracking (destructive, confirmed).
14
+ *
15
+ * SDK operations: none of its own. Composes IntentFeed (read), AccountIntentScore / useAccountIntent (read),
16
+ * IntentKeywordManager (billable + destructive, every call confirmed) and one read of listAbmCompanies for the stat strip.
17
+ * Prerequisite: ABM intent enabled for the workspace (a 403 is explained inline), and something to measure.
18
+ */
19
+ import { useState } from "react";
20
+ import type { ReactNode } from "react";
21
+ import { Button } from "@graph8/sdk/ui";
22
+ import { Radar, Tags, X } from "lucide-react";
23
+ import { g8Get } from "@/g8/lib/g8-client";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { useUrlState } from "@/g8/hooks/use-url-state";
26
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
27
+ import type { StatItem } from "@/g8/components/screen/stat-strip";
28
+ import { SplitView } from "@/g8/components/screen/split-view";
29
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
30
+ import { ApiError } from "@/g8/components/api-error";
31
+ import { EmptyState } from "@/g8/components/empty-state";
32
+ import { Toolbar } from "@/g8/components/screen/toolbar";
33
+ import { StatusPill } from "@/g8/components/screen/status-pill";
34
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
35
+ import { SectionCard } from "@/g8/components/screen/section-card";
36
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
37
+ import { BarList } from "@/g8/components/screen/bar-list";
38
+ import { useAccountIntent } from "../hooks/use-account-intent";
39
+ import { formatDate, rowsOf, totalOf } from "../lib/intent-utils";
40
+ import type { AbmAccount, IntentKeywordRow } from "../lib/intent-utils";
41
+ import { AccountIntentScore } from "./account-intent-score";
42
+ import { IntentFeed } from "./intent-feed";
43
+ import { IntentKeywordManager } from "./intent-keyword-manager";
44
+ import { KeywordDetail } from "./keyword-detail";
45
+
46
+ export interface IntentScreenProps {
47
+ /** Extra buttons for the toolbar (for example "Add to list" glue from another block). */
48
+ toolbarActions?: ReactNode;
49
+ /** Buttons shown in the account detail header. */
50
+ renderDetailActions?: (account: AbmAccount) => ReactNode;
51
+ /** Extra tabs in the account detail panel. */
52
+ extraDetailTabs?: { id: string; label: string; render: (account: AbmAccount) => ReactNode }[];
53
+ className?: string;
54
+ }
55
+
56
+ const WINDOWS = [7, 30, 90] as const;
57
+ type Tab = "accounts" | "keywords";
58
+
59
+ export function strengthTone(strength: string | null | undefined): "positive" | "warning" | "neutral" {
60
+ const s = (strength ?? "").toLowerCase();
61
+ if (s.includes("strong") || s === "high") return "positive";
62
+ if (s.includes("moderate") || s === "medium") return "warning";
63
+ return "neutral";
64
+ }
65
+
66
+ interface PanelProps {
67
+ onClose: () => void;
68
+ onExpand?: () => void;
69
+ onCollapse?: () => void;
70
+ }
71
+
72
+ function AccountDetail({ account, panel, props }: { account: AbmAccount; panel: PanelProps; props: IntentScreenProps }) {
73
+ const name = account.company_name ?? account.company_domain ?? "Unknown company";
74
+ const keywords = (account.top_keywords ?? []).map((k) => ({ label: k.keyword ?? "keyword", value: k.signal_count ?? 0 }));
75
+ const trend = typeof account.trend_pct === "number" ? `${account.trend_pct > 0 ? "+" : ""}${Math.round(account.trend_pct)}%` : "No trend yet";
76
+ const tabs = [
77
+ {
78
+ id: "signals",
79
+ label: "Signals",
80
+ content: (
81
+ <div className="space-y-4">
82
+ <KeyValueList
83
+ columns={2}
84
+ items={[
85
+ { label: "Domain", value: account.company_domain ?? "-" },
86
+ { label: "Industry", value: account.industry ?? "-" },
87
+ { label: "Employees", value: account.employee_count ?? "-" },
88
+ { label: "Signals", value: account.total_signal_count ?? 0 },
89
+ { label: "Trend", value: trend },
90
+ { label: "Last signal", value: formatDate(account.latest_signal_date) },
91
+ ]}
92
+ />
93
+ <SectionCard title="Top keywords" description="Where this account's interest comes from.">
94
+ {keywords.length ? <BarList items={keywords} /> : <p className="text-sm text-muted-foreground">No keyword breakdown for this account.</p>}
95
+ </SectionCard>
96
+ </div>
97
+ ),
98
+ },
99
+ {
100
+ id: "committee",
101
+ label: "Buying committee",
102
+ content:
103
+ account.company_id != null ? (
104
+ <AccountIntentScore companyIds={[account.company_id]} />
105
+ ) : (
106
+ <p className="text-sm text-muted-foreground">This account has no company id to score.</p>
107
+ ),
108
+ },
109
+ ...(props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(account) })),
110
+ ];
111
+ return (
112
+ <DetailPanel
113
+ title={name}
114
+ subtitle={account.company_domain ?? undefined}
115
+ avatar={<EntityAvatar name={name} imageUrl={account.logo_url ?? undefined} kind="company" size="lg" />}
116
+ badges={
117
+ <>
118
+ <StatusPill tone={strengthTone(account.account_strength)}>{account.account_strength ?? "unscored"}</StatusPill>
119
+ {account.is_high_intent ? <StatusPill tone="negative" dot>High intent</StatusPill> : null}
120
+ </>
121
+ }
122
+ actions={props.renderDetailActions?.(account)}
123
+ tabs={tabs}
124
+ onClose={panel.onClose}
125
+ onExpand={panel.onExpand}
126
+ onCollapse={panel.onCollapse}
127
+ />
128
+ );
129
+ }
130
+
131
+ function useStats(days: number): { items: StatItem[] | undefined; loading: boolean } {
132
+ const q = useG8Query((signal) => g8Get("/intent/abm/companies", { days, page: 1, limit: 100 }, { signal }), [days]);
133
+ if (q.error || !q.data) return { items: undefined, loading: q.loading };
134
+ // The ABM payload is open-shaped in the contract: only report what the rows actually carry.
135
+ const rows = rowsOf<AbmAccount>(q.data);
136
+ const total = totalOf(q.data) ?? rows.length;
137
+ const items: StatItem[] = [{ label: "Accounts with intent", value: total.toLocaleString(), hint: `last ${days} days` }];
138
+ if (rows.some((r) => typeof r.is_high_intent === "boolean")) {
139
+ const high = rows.filter((r) => r.is_high_intent).length;
140
+ items.push({ label: "High intent", value: high, tone: high ? "negative" : "neutral", hint: "in the first 100" });
141
+ }
142
+ if (rows.some((r) => typeof r.account_strength === "string")) {
143
+ const strong = rows.filter((r) => strengthTone(r.account_strength) === "positive").length;
144
+ items.push({ label: "Strong accounts", value: strong, tone: strong ? "positive" : "neutral", hint: "in the first 100" });
145
+ }
146
+ return { loading: q.loading, items };
147
+ }
148
+
149
+ export function IntentScreen(props: IntentScreenProps) {
150
+ const [tab, setTab] = useUrlState("tab", "accounts");
151
+ const [id, setId] = useUrlState("id", "");
152
+ const [view, setView] = useUrlState("view", "");
153
+ const [days, setDays] = useState<number>(30);
154
+ const [selected, setSelected] = useState<AbmAccount | null>(null);
155
+ const [keyword, setKeyword] = useState<IntentKeywordRow | null>(null);
156
+ // `?kw=<keyword id>` scopes the account feed to one tracked keyword (chip in the toolbar).
157
+ const [scopeId, setScopeId] = useUrlState("kw", "");
158
+ const [scopeLabel, setScopeLabel] = useState<string | null>(null);
159
+ const [removed, setRemoved] = useState<string[]>([]);
160
+ const stats = useStats(days);
161
+ const active: Tab = tab === "keywords" ? "keywords" : "accounts";
162
+ const expanded = view === "full" && Boolean(id);
163
+ // A non-numeric `?id=` opens nothing instead of asking for account NaN.
164
+ const idNumber = id && Number.isFinite(Number(id)) ? Number(id) : null;
165
+ const deepLinked = useAccountIntent(active === "accounts" && idNumber != null && !selected ? [idNumber] : [], days);
166
+ const account = selected ?? (active === "accounts" && idNumber != null && deepLinked.rollups[id] ? { ...deepLinked.rollups[id], company_id: idNumber } : null);
167
+ // A deep-linked account with no signal in this window (or a bad id) gets a not-found state instead of a silent no-op.
168
+ const accountMissing = active === "accounts" && Boolean(id) && !account && (idNumber == null || (!deepLinked.loading && !deepLinked.error));
169
+ const accountFailed = active === "accounts" && Boolean(id) && !account && idNumber != null && Boolean(deepLinked.error);
170
+
171
+ const close = () => {
172
+ setSelected(null);
173
+ setKeyword(null);
174
+ setId("");
175
+ setView("");
176
+ };
177
+ const panel: PanelProps = {
178
+ onClose: close,
179
+ onExpand: expanded ? undefined : () => setView("full"),
180
+ onCollapse: expanded ? () => setView("") : undefined,
181
+ };
182
+ const showAccounts = (k: IntentKeywordRow) => {
183
+ close();
184
+ setScopeId(k.id);
185
+ setScopeLabel(k.keyword);
186
+ setTab("accounts");
187
+ };
188
+ const openAccount = (a: AbmAccount) => {
189
+ setTab("accounts");
190
+ setKeyword(null);
191
+ setSelected(a);
192
+ setId(String(a.company_id ?? ""));
193
+ };
194
+
195
+ return (
196
+ <ScreenLayout
197
+ title="Intent"
198
+ description="Accounts researching what you sell, and the keywords and competitors you track to find them."
199
+ icon={<Radar className="size-5" />}
200
+ actions={
201
+ <Button onClick={() => { setTab("keywords"); close(); }}>
202
+ <Tags className="mr-1.5 size-4" /> Track keywords
203
+ </Button>
204
+ }
205
+ tabs={[
206
+ { id: "accounts", label: "Accounts" },
207
+ { id: "keywords", label: "Keywords" },
208
+ ]}
209
+ activeTab={active}
210
+ onTabChange={(next) => {
211
+ setTab(next);
212
+ close();
213
+ }}
214
+ stats={active === "accounts" ? stats.items : undefined}
215
+ className={props.className}
216
+ >
217
+ {active === "accounts" ? (
218
+ <div className="space-y-3">
219
+ <Toolbar actions={props.toolbarActions}>
220
+ {scopeId ? (
221
+ <Button size="sm" variant="outline" aria-label={`Remove keyword scope ${scopeLabel ?? scopeId}`} onClick={() => { setScopeId(null); setScopeLabel(null); }}>
222
+ Keyword: {scopeLabel ?? scopeId} <X className="ml-1 size-3.5" aria-hidden />
223
+ </Button>
224
+ ) : null}
225
+ <div className="inline-flex gap-1" role="group" aria-label="Look-back window">
226
+ {WINDOWS.map((w) => (
227
+ <Button key={w} size="sm" variant={days === w ? "default" : "outline"} aria-pressed={days === w} onClick={() => setDays(w)}>
228
+ {w} days
229
+ </Button>
230
+ ))}
231
+ </div>
232
+ </Toolbar>
233
+ <SplitView
234
+ detailOpen={Boolean(account) || accountMissing || accountFailed}
235
+ onCloseDetail={close}
236
+ expanded={expanded && Boolean(account)}
237
+ onExpandedChange={(next) => setView(next ? "full" : "")}
238
+ detailWidth="lg"
239
+ list={<IntentFeed key={scopeId || "all"} days={days} keywordIds={scopeId ? [scopeId] : undefined} onSelectAccount={openAccount} />}
240
+ detail={account ? (
241
+ <AccountDetail key={account.company_id ?? account.company_domain} account={account} panel={panel} props={props} />
242
+ ) : accountFailed ? (
243
+ <div className="space-y-3 p-4"><ApiError error={deepLinked.error} title="Could not load this account's intent" onRetry={() => void deepLinked.refetch()} /><Button size="sm" variant="outline" onClick={close}>Close</Button></div>
244
+ ) : accountMissing ? (
245
+ <div className="space-y-3 p-4"><EmptyState title="No intent for this account" description={`No buying-intent signal for this account in the last ${days} days. Try a longer window.`} /><Button size="sm" variant="outline" onClick={close}>Close</Button></div>
246
+ ) : null}
247
+ />
248
+ </div>
249
+ ) : (
250
+ <SplitView
251
+ detailOpen={Boolean(id)}
252
+ onCloseDetail={close}
253
+ expanded={expanded}
254
+ onExpandedChange={(next) => setView(next ? "full" : "")}
255
+ detailWidth="lg"
256
+ list={<IntentKeywordManager hiddenIds={removed} onSelectKeyword={(k) => { setKeyword(k); setId(k.id); }} />}
257
+ detail={id ? (
258
+ <KeywordDetail key={id} keywordId={id} keyword={keyword?.id === id ? keyword : null} onLoaded={setKeyword}
259
+ onSelectAccount={openAccount} onShowAccounts={showAccounts} onRemoved={(kid) => setRemoved((r) => [...r, kid])} {...panel} />
260
+ ) : null}
261
+ />
262
+ )}
263
+ </ScreenLayout>
264
+ );
265
+ }
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ /**
4
+ * KeywordDetail: the complete screen for one tracked keyword or competitor domain. Shows every field the list returns, the
5
+ * accounts currently showing intent for it (the intent feed scoped to this keyword), the companies resolved behind it, and the
6
+ * action to stop tracking. Pass the row from IntentKeywordManager; with only an id (a deep link) it offers to load the row.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listAbmCompanies GET /intent/abm/companies?keyword_ids (read; inside IntentFeed)
10
+ * 2. keywordsList POST /intent/keywords/list (billable tier, confirmed; only to restore a row from an id)
11
+ * 3. keywordCompanies POST /intent/keywords/{id}/companies (billable, confirmed)
12
+ * 4. keywordsDelete DELETE /intent/keywords/{id} (destructive, confirmed; 404 counts as done)
13
+ */
14
+ import { useState } from "react";
15
+ import { Button } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { EmptyState } from "@/g8/components/empty-state";
18
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
19
+ import { g8Delete, g8Post } from "@/g8/lib/g8-client";
20
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
21
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
22
+ import { SectionCard } from "@/g8/components/screen/section-card";
23
+ import { StatusPill } from "@/g8/components/screen/status-pill";
24
+ import { rowsOf } from "../lib/intent-utils";
25
+ import type { AbmAccount, IntentKeywordRow } from "../lib/intent-utils";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
28
+ import { IntentFeed } from "./intent-feed";
29
+
30
+ export interface KeywordDetailProps {
31
+ keywordId: string;
32
+ /** The row from the keyword list. Without it the screen can restore it after a confirmed list call. */
33
+ keyword?: IntentKeywordRow | null;
34
+ /** Called with the restored row. */
35
+ onLoaded?: (keyword: IntentKeywordRow) => void;
36
+ onSelectAccount?: (account: AbmAccount) => void;
37
+ onClose?: () => void;
38
+ onExpand?: () => void;
39
+ onCollapse?: () => void;
40
+ /** Called after the keyword was removed. */
41
+ onRemoved?: (keywordId: string) => void;
42
+ /** Shows a "Show accounts" button that scopes the account feed to this keyword. */
43
+ onShowAccounts?: (keyword: IntentKeywordRow) => void;
44
+ }
45
+
46
+ interface CompanyRow {
47
+ name?: string | null;
48
+ company_name?: string | null;
49
+ COMPANY_NAME?: string | null;
50
+ domain?: string | null;
51
+ company_domain?: string | null;
52
+ COMPANY_DOMAIN?: string | null;
53
+ }
54
+ const companyName = (c: CompanyRow) => c.name ?? c.company_name ?? c.COMPANY_NAME ?? c.domain ?? c.company_domain ?? c.COMPANY_DOMAIN ?? "Unknown";
55
+
56
+ export function KeywordDetail({ keywordId, keyword, onLoaded, onSelectAccount, onClose, onExpand, onCollapse, onRemoved, onShowAccounts }: KeywordDetailProps) {
57
+ const { ask, dialog, failure } = useConfirmAction();
58
+ const [companies, setCompanies] = useState<CompanyRow[] | null>(null);
59
+ const restore = useG8Mutation(async () => {
60
+ const found = rowsOf<IntentKeywordRow>(await g8Post("/intent/keywords/list", { page: 1, limit: 200 })).find((k) => k.id === keywordId);
61
+ if (found) onLoaded?.(found);
62
+ return found ?? null;
63
+ });
64
+
65
+ if (!keyword) {
66
+ return (
67
+ <div className="space-y-3 p-4">
68
+ {restore.data === null ? (
69
+ <EmptyState title="Keyword not found" description="It is no longer among your tracked keywords. It may have been removed." />
70
+ ) : (
71
+ <EmptyState title="Keyword details are not loaded" description="Listing tracked keywords is a billable read, so it asks first." />
72
+ )}
73
+ <div className="flex justify-center">
74
+ <AskButton ask={ask} label="Load keyword" tier="billable" title="Load tracked keywords" confirmLabel="Load keywords" description="Listing tracked keywords is classified billable by the API."
75
+ run={async () => { await restore.mutateAsync(undefined); }} />
76
+ </div>
77
+ {/* A failed load surfaces once, through the confirm hook's `failure`. */}
78
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
79
+ <ConfirmSpendDialog {...dialog} />
80
+ </div>
81
+ );
82
+ }
83
+
84
+ const actions = (
85
+ <div className="flex flex-wrap gap-1">
86
+ {onShowAccounts ? <Button size="sm" variant="outline" onClick={() => onShowAccounts(keyword)}>Show accounts</Button> : null}
87
+ <AskButton ask={ask} label="Load companies" tier="billable" title={`Load companies for "${keyword.keyword}"`} confirmLabel="Load companies" description="Resolves the companies behind this keyword's traffic."
88
+ run={async () => setCompanies(rowsOf<CompanyRow>(await g8Post(`/intent/keywords/${keyword.id}/companies`, { limit: 50, page: 1 })))} />
89
+ <AskButton ask={ask} label="Stop tracking" variant="ghost" tier="destructive" title={`Stop tracking "${keyword.keyword}"`} confirmLabel="Stop tracking" tolerateStatus={[404]}
90
+ description="The keyword is removed. Companies already saved to lists stay." run={() => g8Delete(`/intent/keywords/${keyword.id}`)} onDone={() => { onRemoved?.(keyword.id); onClose?.(); }} />
91
+ </div>
92
+ );
93
+ const tabs = [
94
+ {
95
+ id: "overview",
96
+ label: "Overview",
97
+ content: (
98
+ <div className="space-y-4">
99
+ <KeyValueList columns={2} items={[
100
+ { label: "Keyword", value: keyword.keyword },
101
+ { label: "Type", value: keyword.signal_type ?? "-" },
102
+ { label: "Companies resolved", value: keyword.total_resolved_companies ?? 0 },
103
+ { label: "People resolved", value: keyword.total_resolved_contacts ?? 0 },
104
+ { label: "Pages tracked", value: keyword.total_urls ?? "-" },
105
+ { label: "Auto sync", value: keyword.auto_sync_enabled == null ? "-" : keyword.auto_sync_enabled ? "on" : "off" },
106
+ ]} />
107
+ <SectionCard title="Companies behind this keyword" description="Loading them is billable, so it asks first (Load companies above).">
108
+ {companies == null ? <p className="text-sm text-muted-foreground">Not loaded yet.</p> : companies.length === 0 ? <p className="text-sm text-muted-foreground">No resolved companies yet.</p> : (
109
+ <ul className="grid gap-1 text-sm md:grid-cols-2">{companies.map((c, i) => <li key={i}>{companyName(c)}</li>)}</ul>
110
+ )}
111
+ </SectionCard>
112
+ </div>
113
+ ),
114
+ },
115
+ { id: "accounts", label: "Accounts showing intent", content: <IntentFeed keywordIds={[keyword.id]} onSelectAccount={onSelectAccount} /> },
116
+ ];
117
+ return (
118
+ <>
119
+ <DetailPanel title={keyword.keyword} subtitle="Tracked keyword" badges={<StatusPill tone={keyword.is_active === false ? "neutral" : "positive"}>{keyword.is_active === false ? "inactive" : "tracking"}</StatusPill>} actions={actions} tabs={tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
120
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
121
+ <ConfirmSpendDialog {...dialog} />
122
+ </>
123
+ );
124
+ }
@@ -13,6 +13,22 @@ These five are ONE parametrised view keyed by stage, not five features: the stag
13
13
  | `ChurnList` | Churned companies, last MRR, on-demand activity check, win-back to Leads. |
14
14
  | `useLifecycleStages` (hook) | Stages and their counts. |
15
15
 
16
+ ## Screens
17
+
18
+ `LifecycleScreen` (`components/lifecycle-screen.tsx`): `<LifecycleScreen />`.
19
+
20
+ - A count per stage as headline numbers, then a Board tab and one tab per stage (custom stages included).
21
+ - Board: columns of companies with Move (a dry run shows refusals first, for example Customers without an active
22
+ subscription). Customers tab: the book of business with MRR, headline counts and the account brief (rewriting it
23
+ uses credits, confirmed). Churned tab: lost revenue, an activity check and a win-back to Leads. Every other stage is
24
+ a contact queue with search; locked emails are unlocked with credits (confirmed) and contacts can be moved.
25
+ - Open a company or contact (deep link `?rec=company:<id>` or `?rec=contact:<id>`) to get the crm block's full record
26
+ (activity, notes, tasks, edit) with a **Move stage** action.
27
+ - Slots, all optional: `renderDetailActions(record)`, `extraDetailTabs`, `toolbarActions`.
28
+
29
+ Proxy `allowedPrefixes` must also include the crm block's prefixes for the record panel.
30
+
31
+
16
32
  ## Flow, in order (and the ids passed between steps)
17
33
 
18
34
  1. `GET /lifecycle-stages` -> each stage has `id` (entry id) and `slug` (`suspects`, `leads`, `trials`, `customers`, `churned`).
@@ -13,17 +13,18 @@
13
13
  "crm"
14
14
  ],
15
15
  "operations": [
16
- "list_lifecycle_stages_lifecycle_stages_get",
16
+ "get_account_intent_signals_accounts__account_id__intent_signals_get",
17
+ "get_brief_customer_success_companies__company_id__brief_get",
18
+ "get_company_companies__company_id__get",
17
19
  "get_lifecycle_stage_counts_lifecycle_stages_counts_get",
20
+ "get_revenue_summary_accounts_revenue_summary_get",
21
+ "list_lifecycle_contacts_lifecycle_contacts_get",
18
22
  "list_lifecycle_stage_records_lifecycle_stages__stage__records_get",
23
+ "list_lifecycle_stages_lifecycle_stages_get",
19
24
  "move_company_lifecycle_stage_companies__company_id__lifecycle_stage_post",
20
25
  "move_contact_lifecycle_stage_contacts__contact_id__lifecycle_stage_post",
21
- "list_lifecycle_contacts_lifecycle_contacts_get",
22
- "unlock_lifecycle_contacts_lifecycle_contacts_unlock_post",
23
- "get_revenue_summary_accounts_revenue_summary_get",
24
- "get_brief_customer_success_companies__company_id__brief_get",
25
26
  "synthesize_brief_customer_success_companies__company_id__brief_synthesize_post",
26
- "get_account_intent_signals_accounts__account_id__intent_signals_get"
27
+ "unlock_lifecycle_contacts_lifecycle_contacts_unlock_post"
27
28
  ],
28
29
  "components": [
29
30
  {
@@ -98,6 +99,31 @@
98
99
  "read",
99
100
  "write"
100
101
  ]
102
+ },
103
+ {
104
+ "name": "LifecycleScreen",
105
+ "file": "components/lifecycle-screen.tsx",
106
+ "purpose": "Finished lifecycle product: board, one tab per stage (contact queue, customers, churned) and a CRM record panel with Move stage.",
107
+ "operations": [
108
+ "get_company_companies__company_id__get"
109
+ ],
110
+ "tiers": [
111
+ "read"
112
+ ]
113
+ },
114
+ {
115
+ "name": "AccountBriefTab",
116
+ "file": "components/account-related.tsx",
117
+ "purpose": "Account views for the record panel: the customer-success brief (rewrite is billable and confirmed) and AccountSignalsTab, recent intent signals.",
118
+ "operations": [
119
+ "get_brief_customer_success_companies__company_id__brief_get",
120
+ "synthesize_brief_customer_success_companies__company_id__brief_synthesize_post",
121
+ "get_account_intent_signals_accounts__account_id__intent_signals_get"
122
+ ],
123
+ "tiers": [
124
+ "read",
125
+ "billable"
126
+ ]
101
127
  }
102
128
  ],
103
129
  "hooks": [
@@ -193,6 +219,7 @@
193
219
  }
194
220
  ],
195
221
  "peerDependencies": {
196
- "react": ">=18"
222
+ "react": ">=18",
223
+ "lucide-react": ">=0.400.0"
197
224
  }
198
225
  }