@graph8/sdk 0.362.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 (349) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +64 -1
  3. package/blocks/_foundation/block.json +79 -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/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/screen/index.ts +1 -0
  9. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  10. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  11. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  12. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  13. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  14. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  15. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  16. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  17. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  18. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  19. package/blocks/_foundation/lib/g8-client.ts +1 -1
  20. package/blocks/_foundation/lib/plural.ts +23 -0
  21. package/blocks/_foundation/server/connect-route.ts +15 -0
  22. package/blocks/_foundation/server/connect.ts +127 -0
  23. package/blocks/_foundation/server/local-key.ts +70 -0
  24. package/blocks/_foundation/server/proxy.ts +103 -3
  25. package/blocks/_foundation/server/route.ts +16 -4
  26. package/blocks/agents/README.md +14 -0
  27. package/blocks/agents/block.json +33 -1
  28. package/blocks/agents/components/agent-builder.tsx +13 -8
  29. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  30. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  31. package/blocks/agents/components/agent-runs.tsx +16 -7
  32. package/blocks/agents/components/agents-screen.tsx +184 -0
  33. package/blocks/analytics/README.md +13 -0
  34. package/blocks/analytics/block.json +45 -1
  35. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  36. package/blocks/analytics/components/leaderboard.tsx +14 -7
  37. package/blocks/analytics/components/report-builder.tsx +23 -9
  38. package/blocks/analytics/components/report-detail.tsx +84 -0
  39. package/blocks/analytics/components/trend-chart.tsx +62 -0
  40. package/blocks/appointments/block.json +6 -2
  41. package/blocks/appointments/components/availability-editor.tsx +17 -5
  42. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  43. package/blocks/appointments/components/event-type-related.tsx +4 -2
  44. package/blocks/appointments/components/meeting-list.tsx +9 -5
  45. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  46. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  47. package/blocks/block.schema.json +18 -0
  48. package/blocks/block.types.ts +17 -0
  49. package/blocks/campaigns/README.md +41 -11
  50. package/blocks/campaigns/block.json +105 -42
  51. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  52. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  53. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  54. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  55. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  56. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  57. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  58. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  59. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  60. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  61. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  62. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  63. package/blocks/coach/README.md +10 -0
  64. package/blocks/coach/block.json +16 -1
  65. package/blocks/coach/components/coach-screen.tsx +222 -0
  66. package/blocks/copilot/README.md +13 -0
  67. package/blocks/copilot/block.json +14 -1
  68. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  69. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  70. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  71. package/blocks/crm/README.md +28 -0
  72. package/blocks/crm/block.json +58 -2
  73. package/blocks/crm/components/companies-screen.tsx +345 -0
  74. package/blocks/crm/components/company-detail.tsx +8 -6
  75. package/blocks/crm/components/company-table.tsx +11 -4
  76. package/blocks/crm/components/contact-detail.tsx +6 -3
  77. package/blocks/crm/components/contact-table.tsx +10 -3
  78. package/blocks/crm/components/contacts-screen.tsx +359 -0
  79. package/blocks/crm/components/dedupe-review.tsx +7 -5
  80. package/blocks/crm/components/import-wizard.tsx +4 -2
  81. package/blocks/crm/components/notes-panel.tsx +4 -4
  82. package/blocks/crm/components/record-form.tsx +5 -3
  83. package/blocks/crm/components/task-list.tsx +4 -4
  84. package/blocks/crm/components/tasks-screen.tsx +379 -0
  85. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  86. package/blocks/crm/hooks/use-page-query.ts +7 -1
  87. package/blocks/crm/lib/crm-types.ts +12 -2
  88. package/blocks/crm/lib/screen-parts.tsx +105 -0
  89. package/blocks/deals/block.json +9 -3
  90. package/blocks/deals/components/deal-detail.tsx +23 -11
  91. package/blocks/deals/components/deal-related.tsx +1 -1
  92. package/blocks/deals/components/deals-screen.tsx +10 -5
  93. package/blocks/deals/components/stage-editor.tsx +4 -4
  94. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  95. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  96. package/blocks/dialer/README.md +34 -2
  97. package/blocks/dialer/block.json +67 -1
  98. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  99. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  100. package/blocks/dialer/components/call-log.tsx +7 -3
  101. package/blocks/dialer/components/call-queue.tsx +1 -1
  102. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  103. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  104. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  105. package/blocks/dialer/components/session-start-form.tsx +4 -2
  106. package/blocks/dialer/components/softphone.tsx +3 -1
  107. package/blocks/enrichment/README.md +14 -2
  108. package/blocks/enrichment/block.json +32 -4
  109. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  110. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  111. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  112. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  113. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  114. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  115. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  116. package/blocks/forms/block.json +32 -18
  117. package/blocks/forms/components/form-builder.tsx +3 -1
  118. package/blocks/forms/components/form-embed.tsx +3 -1
  119. package/blocks/forms/components/forms-screen.tsx +5 -2
  120. package/blocks/forms/components/submissions-table.tsx +3 -1
  121. package/blocks/inbox/README.md +27 -3
  122. package/blocks/inbox/block.json +14 -2
  123. package/blocks/inbox/components/inbox-list.tsx +7 -3
  124. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  125. package/blocks/inbox/components/reply-composer.tsx +65 -9
  126. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  127. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  128. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  129. package/blocks/inbox/lib/inbox-types.ts +47 -1
  130. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  131. package/blocks/integrations/README.md +9 -0
  132. package/blocks/integrations/block.json +46 -1
  133. package/blocks/integrations/components/crm-detail.tsx +100 -0
  134. package/blocks/integrations/components/integration-list.tsx +9 -4
  135. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  136. package/blocks/integrations/components/sync-status.tsx +5 -3
  137. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  138. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  139. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  140. package/blocks/intent/README.md +14 -0
  141. package/blocks/intent/block.json +27 -1
  142. package/blocks/intent/components/intent-feed.tsx +5 -2
  143. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  144. package/blocks/intent/components/intent-screen.tsx +265 -0
  145. package/blocks/intent/components/keyword-detail.tsx +124 -0
  146. package/blocks/lifecycle/block.json +13 -8
  147. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  148. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  149. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  150. package/blocks/lists/README.md +13 -0
  151. package/blocks/lists/block.json +39 -1
  152. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  153. package/blocks/lists/components/list-picker.tsx +5 -1
  154. package/blocks/lists/components/list-table.tsx +11 -5
  155. package/blocks/lists/components/lists-screen.tsx +344 -0
  156. package/blocks/lists/components/suppression-manager.tsx +6 -2
  157. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  158. package/blocks/mailboxes/README.md +17 -0
  159. package/blocks/mailboxes/block.json +65 -17
  160. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  161. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  162. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  163. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  164. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  165. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  166. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  167. package/blocks/newsletter/block.json +40 -24
  168. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  169. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  170. package/blocks/nurture/block.json +28 -14
  171. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  172. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  173. package/blocks/nurture/components/nurture-form.tsx +23 -3
  174. package/blocks/nurture/components/nurture-screen.tsx +10 -6
  175. package/blocks/nurture/lib/nurture-types.ts +1 -1
  176. package/blocks/objects/README.md +17 -0
  177. package/blocks/objects/block.json +29 -16
  178. package/blocks/objects/components/object-not-found.tsx +3 -1
  179. package/blocks/objects/components/object-record-detail.tsx +2 -1
  180. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  181. package/blocks/objects/components/object-table.tsx +13 -3
  182. package/blocks/objects/components/objects-screen.tsx +147 -0
  183. package/blocks/objects/lib/object-types.ts +1 -1
  184. package/blocks/pages/block.json +28 -23
  185. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  186. package/blocks/pages/components/content-library.tsx +9 -4
  187. package/blocks/pages/components/page-list.tsx +2 -1
  188. package/blocks/quotes/block.json +17 -12
  189. package/blocks/quotes/components/product-picker.tsx +3 -1
  190. package/blocks/quotes/components/quote-related.tsx +5 -2
  191. package/blocks/radar/README.md +10 -0
  192. package/blocks/radar/block.json +41 -1
  193. package/blocks/radar/components/competitor-detail.tsx +117 -0
  194. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  195. package/blocks/radar/components/radar-report.tsx +4 -2
  196. package/blocks/radar/components/radar-screen.tsx +214 -0
  197. package/blocks/radar/components/report-detail.tsx +104 -0
  198. package/blocks/search/README.md +10 -0
  199. package/blocks/search/block.json +16 -1
  200. package/blocks/search/components/filter-builder.tsx +3 -3
  201. package/blocks/search/components/prospect-results.tsx +5 -3
  202. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  203. package/blocks/search/components/saved-searches.tsx +4 -1
  204. package/blocks/search/hooks/use-filter-options.ts +20 -6
  205. package/blocks/search/lib/search-types.ts +2 -0
  206. package/blocks/sequencer/README.md +49 -9
  207. package/blocks/sequencer/block.json +99 -19
  208. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  209. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  210. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  211. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  212. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  213. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  214. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  215. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  216. package/blocks/sequencer/components/step-editor.tsx +3 -1
  217. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  218. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  219. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  220. package/blocks/social-ads/README.md +11 -0
  221. package/blocks/social-ads/block.json +101 -2
  222. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  223. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  224. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  225. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  226. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  227. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  228. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  229. package/blocks/social-ads/components/post-composer.tsx +3 -1
  230. package/blocks/social-ads/components/post-detail.tsx +160 -0
  231. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  232. package/blocks/social-ads/components/social-screen.tsx +160 -0
  233. package/blocks/visitors/README.md +10 -0
  234. package/blocks/visitors/block.json +25 -1
  235. package/blocks/visitors/components/snippet-install.tsx +3 -1
  236. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  237. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  238. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  239. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  240. package/blocks/webchat/README.md +10 -0
  241. package/blocks/webchat/block.json +21 -7
  242. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  243. package/blocks/webchat/components/conversation-list.tsx +7 -3
  244. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  245. package/blocks/workbench/README.md +11 -0
  246. package/blocks/workbench/block.json +48 -1
  247. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  248. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  249. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  250. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  251. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  252. package/blocks/workflows/README.md +13 -0
  253. package/blocks/workflows/block.json +28 -9
  254. package/blocks/workflows/components/run-history.tsx +31 -9
  255. package/blocks/workflows/components/trigger-config.tsx +3 -1
  256. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  257. package/blocks/workflows/components/workflow-list.tsx +13 -6
  258. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  259. package/dist/cli.js +2709 -1721
  260. package/dist/errors.d.mts +34 -0
  261. package/dist/errors.d.ts +34 -0
  262. package/dist/errors.js +46 -0
  263. package/dist/errors.js.map +1 -0
  264. package/dist/errors.mjs +21 -0
  265. package/dist/errors.mjs.map +1 -0
  266. package/dist/index.browser.js +11 -5
  267. package/dist/index.browser.js.map +1 -1
  268. package/dist/index.browser.mjs +11 -5
  269. package/dist/index.browser.mjs.map +1 -1
  270. package/dist/index.d.mts +164 -25
  271. package/dist/index.d.ts +164 -25
  272. package/dist/index.js +11 -5
  273. package/dist/index.js.map +1 -1
  274. package/dist/index.mjs +11 -5
  275. package/dist/index.mjs.map +1 -1
  276. package/dist/react.browser.js +10 -4
  277. package/dist/react.browser.js.map +1 -1
  278. package/dist/react.browser.mjs +10 -4
  279. package/dist/react.browser.mjs.map +1 -1
  280. package/dist/react.d.mts +142 -5
  281. package/dist/react.d.ts +142 -5
  282. package/dist/react.js +10 -4
  283. package/dist/react.js.map +1 -1
  284. package/dist/react.mjs +10 -4
  285. package/dist/react.mjs.map +1 -1
  286. package/dist/types/blocks/add.d.ts +11 -4
  287. package/dist/types/blocks/brand.d.ts +113 -0
  288. package/dist/types/blocks/compose.d.ts +53 -0
  289. package/dist/types/blocks/create.d.ts +22 -19
  290. package/dist/types/blocks/doctor.d.ts +5 -1
  291. package/dist/types/blocks/navfile.d.ts +35 -0
  292. package/dist/types/blocks/paths.d.ts +4 -0
  293. package/dist/types/blocks/prefixes.d.ts +37 -0
  294. package/dist/types/blocks/registry.d.ts +17 -0
  295. package/dist/types/blocks/screens.d.ts +53 -0
  296. package/dist/types/cli.d.ts +2 -0
  297. package/dist/types/errors.d.ts +33 -0
  298. package/dist/types/http.d.ts +2 -18
  299. package/dist/types/version.d.ts +2 -2
  300. package/docs/agents.md +15 -4
  301. package/docs/api/README.md +2 -2
  302. package/docs/api/by-path.md +1 -0
  303. package/docs/api/desk.d.ts +98 -2
  304. package/docs/api/desk.md +123 -1
  305. package/docs/api/inbox.d.ts +10 -2
  306. package/docs/api/inbox.md +10 -2
  307. package/docs/api/seo.d.ts +19 -1
  308. package/docs/api/seo.md +24 -1
  309. package/package.json +16 -2
  310. package/stacks/6sense.json +41 -16
  311. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  312. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  313. package/stacks/apollo.json +118 -36
  314. package/stacks/calendly.json +60 -0
  315. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  316. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  317. package/stacks/clay.json +59 -26
  318. package/stacks/gong.json +81 -35
  319. package/stacks/hootsuite.json +99 -0
  320. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  321. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  322. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  323. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  324. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  325. package/stacks/hubspot.json +167 -66
  326. package/stacks/intercom.json +48 -20
  327. package/stacks/mailchimp.json +52 -33
  328. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  329. package/stacks/outreach.json +88 -25
  330. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  331. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  332. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  333. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  334. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  335. package/stacks/pipedrive.json +115 -0
  336. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  337. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  338. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  339. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  340. package/stacks/salesforce.json +81 -31
  341. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  342. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  343. package/stacks/zoominfo.json +102 -0
  344. package/styles/styles.css +1 -1
  345. package/templates/next-app/README.md.tmpl +40 -9
  346. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  347. package/templates/next-app/components/app-shell.tsx.tmpl +99 -89
  348. package/templates/next-app/dot-gitignore.tmpl +2 -0
  349. package/templates/next-app/package.json.tmpl +1 -1
@@ -26,6 +26,7 @@ import { ApiError } from "@/g8/components/api-error";
26
26
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
27
  import { formatWhen, PROVIDER_LABELS, TERMINAL_JOB_STATES } from "../lib/integrations-utils";
28
28
  import type { CrmProvider, CrmStatus } from "../lib/integrations-utils";
29
+ import { useBrand } from "@/g8/hooks/use-brand";
29
30
 
30
31
  export interface SyncStatusProps {
31
32
  provider: CrmProvider;
@@ -59,6 +60,7 @@ function JobProgress({ jobId }: { jobId: string }) {
59
60
  }
60
61
 
61
62
  export function SyncStatus({ provider, records, className }: SyncStatusProps) {
63
+ const brand = useBrand();
62
64
  const [pending, setPending] = useState<Pending | null>(null);
63
65
  const [jobId, setJobId] = useState<string | null>(null);
64
66
  const [pushed, setPushed] = useState<string | null>(null);
@@ -116,7 +118,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
116
118
  if (!s?.connected) {
117
119
  return (
118
120
  <p role="status" className={`rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900 ${className ?? ""}`}>
119
- {label} is not connected. {s?.message ?? "Connect it in graph8, then come back."}
121
+ {label} is not connected. {s?.message ?? `Connect it in ${brand.linkLabel}, then come back.`}
120
122
  </p>
121
123
  );
122
124
  }
@@ -132,7 +134,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
132
134
  ? {
133
135
  title: `${pending.enabled ? "Turn on" : "Turn off"} continuous ${pending.direction} with ${label}`,
134
136
  description: pending.enabled
135
- ? `graph8 will keep ${pending.direction === "push" ? "writing contacts to" : "reading contacts from"} your CRM.`
137
+ ? `${brand.linkLabel} will keep ${pending.direction === "push" ? "writing contacts to" : "reading contacts from"} your CRM.`
136
138
  : "Syncing in this direction stops.",
137
139
  label: pending.enabled ? "Turn on" : "Turn off",
138
140
  }
@@ -152,7 +154,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
152
154
 
153
155
  {(["push", "pull"] as const).map((direction) => (
154
156
  <div key={direction} className="flex items-center justify-between rounded border border-gray-200 px-3 py-2 text-sm">
155
- <span>{direction === "push" ? "Send graph8 contacts to the CRM" : "Bring CRM contacts into graph8"}</span>
157
+ <span>{direction === "push" ? `Send ${brand.linkLabel} contacts to the CRM` : `Bring CRM contacts into ${brand.linkLabel}`}</span>
156
158
  <span className="flex gap-2">
157
159
  <Button
158
160
  size="sm"
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WebhookDetail: the complete screen for one webhook. Finds it by id (so `?id=` deep links work), shows every field (endpoint, name,
5
+ * events, active state, field filters, created and updated times), its delivery log, and every action: rename, change the endpoint or
6
+ * the events, pause or resume, rotate the signing secret (shown once), delete.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listWebhooks GET /webhooks (read)
10
+ * 2. listWebhookEvents GET /webhooks/events (read; the event names to pick from)
11
+ * 3. listWebhookDeliveries GET /webhooks/{id}/deliveries (read)
12
+ * 4. updateWebhook PATCH /webhooks/{id} (external, confirmed; edit, pause, resume)
13
+ * 5. rotateWebhookSecret POST /webhooks/{id}/rotate-secret (external, confirmed; new secret shown once)
14
+ * 6. deleteWebhook DELETE /webhooks/{id} (destructive, confirmed; 404 counts as done)
15
+ * Verify deliveries on the server with g8.webhooks.constructEvent; the secret never belongs in browser code.
16
+ */
17
+ import { useEffect, useState, type ReactNode } from "react";
18
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
23
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
24
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
25
+ import { SectionCard } from "@/g8/components/screen/section-card";
26
+ import { StatusPill } from "@/g8/components/screen/status-pill";
27
+ import { arrayOf, formatWhen } from "../lib/integrations-utils";
28
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
29
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
30
+ import { useBrand } from "@/g8/hooks/use-brand";
31
+
32
+ export interface WebhookDetailProps {
33
+ webhookId: string;
34
+ onClose?: () => void;
35
+ onExpand?: () => void;
36
+ onCollapse?: () => void;
37
+ onChanged?: () => void;
38
+ /** Extra header actions from the screen's `renderDetailActions` slot. */
39
+ extraActions?: ReactNode;
40
+ /** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
41
+ extraTabs?: { id: string; label: string; content: ReactNode }[];
42
+ }
43
+
44
+ interface Hook { id: string; url: string; name?: string | null; events?: string[]; is_active?: boolean; created_at?: string | null; updated_at?: string | null; field_filters?: Record<string, string[]> | null; secret?: string | null }
45
+ interface EventType { event: string; category?: string | null; description?: string | null }
46
+ interface Delivery { id: string; event: string; status: string; attempts?: number; max_attempts?: number; response_code?: number | null; error_message?: string | null; created_at?: string | null; completed_at?: string | null }
47
+
48
+ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: WebhookDetailProps) {
49
+ const brand = useBrand();
50
+ const { ask, dialog, failure } = useConfirmAction();
51
+ const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
52
+ const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
53
+ const deliveries = useG8Query((signal) => g8Get(`/webhooks/${webhookId}/deliveries`, { page: 1, limit: 20 }, { signal }), [webhookId]);
54
+ const hook = arrayOf<Hook>(hooks.data).find((h) => h.id === webhookId);
55
+ const [form, setForm] = useState({ url: "", name: "", events: [] as string[] });
56
+ const [secret, setSecret] = useState<string | null>(null);
57
+
58
+ useEffect(() => {
59
+ if (hook) setForm({ url: hook.url, name: hook.name ?? "", events: hook.events ?? [] });
60
+ }, [hook?.id, hook?.url, hook?.name, hook?.events?.join(",")]); // eslint-disable-line react-hooks/exhaustive-deps
61
+
62
+ if (hooks.error) return <ApiError error={hooks.error} title="Could not load the webhook" onRetry={() => void hooks.refetch()} />;
63
+ if (!hooks.data) return <Skeleton className="h-40 w-full" />;
64
+ if (!hook) return <EmptyState title="Webhook not found" description="It may have been deleted." />;
65
+ const changed = () => { void hooks.refetch(); onChanged?.(); };
66
+ const active = hook.is_active !== false;
67
+ const byCategory = arrayOf<EventType>(events.data).reduce<Record<string, EventType[]>>((acc, e) => ({ ...acc, [e.category ?? "other"]: [...(acc[e.category ?? "other"] ?? []), e] }), {});
68
+ const toggle = (event: string) => setForm((f) => ({ ...f, events: f.events.includes(event) ? f.events.filter((x) => x !== event) : [...f.events, event] }));
69
+ const urlOk = /^https:\/\/\S+$/.test(form.url.trim());
70
+ const rows = arrayOf<Delivery>(deliveries.data);
71
+
72
+ const actions = (
73
+ <div className="flex flex-wrap gap-1">
74
+ <AskButton ask={ask} label={active ? "Pause" : "Resume"} tier="external" title={`${active ? "Pause" : "Resume"} deliveries to ${hook.url}`} confirmLabel={active ? "Pause" : "Resume"}
75
+ description={active ? "Events stop being sent to this endpoint until you resume." : "Events are sent to this endpoint again."} run={() => g8Patch(`/webhooks/${hook.id}`, { is_active: !active })} onDone={changed} />
76
+ <AskButton ask={ask} label="Rotate secret" tier="external" title={`Rotate the signing secret for ${hook.url}`} confirmLabel="Rotate"
77
+ description="The old secret stops verifying deliveries as soon as you rotate. Update your server before or right after."
78
+ run={async () => { const res = await g8Post<Hook>(`/webhooks/${hook.id}/rotate-secret`); if (res.data?.secret) setSecret(res.data.secret); }} onDone={changed} />
79
+ <AskButton ask={ask} label="Delete" variant="ghost" tier="destructive" title={`Delete the webhook for ${hook.url}`} confirmLabel="Delete" tolerateStatus={[404]}
80
+ description={`${brand.linkLabel} stops sending events to this endpoint and removes its delivery log.`} run={() => g8Delete(`/webhooks/${hook.id}`)} onDone={() => { changed(); onClose?.(); }} />
81
+ </div>
82
+ );
83
+
84
+ const tabs = [
85
+ {
86
+ id: "overview",
87
+ label: "Overview",
88
+ content: (
89
+ <div className="space-y-4">
90
+ {secret ? (
91
+ <div role="alert" className="rounded border border-amber-300 bg-amber-50 p-3 text-sm">
92
+ <p className="font-medium">New signing secret. It is shown once.</p>
93
+ <code className="mt-1 block break-all rounded bg-white p-2">{secret}</code>
94
+ <p className="mt-1 text-xs">Store it as G8_WEBHOOK_SECRET on your server. Never put it in browser code.</p>
95
+ <Button size="sm" variant="outline" className="mt-2" onClick={() => setSecret(null)}>I saved it</Button>
96
+ </div>
97
+ ) : null}
98
+ <KeyValueList columns={2} items={[
99
+ { label: "Endpoint", value: hook.url },
100
+ { label: "Name", value: hook.name ?? "-" },
101
+ { label: "Status", value: active ? "active" : "paused" },
102
+ { label: "Created", value: formatWhen(hook.created_at) },
103
+ { label: "Updated", value: formatWhen(hook.updated_at) },
104
+ { label: "Events", value: (hook.events ?? []).length },
105
+ ]} />
106
+ <SectionCard title="Subscribed events"><ul className="flex flex-wrap gap-1 text-xs">{(hook.events ?? []).map((e) => <li key={e}><StatusPill tone="neutral">{e}</StatusPill></li>)}</ul></SectionCard>
107
+ {hook.field_filters && Object.keys(hook.field_filters).length > 0 ? (
108
+ <SectionCard title="Field filters" description="crm.record.updated fires only when a watched field changed."><pre className="overflow-x-auto text-xs">{JSON.stringify(hook.field_filters, null, 2)}</pre></SectionCard>
109
+ ) : null}
110
+ </div>
111
+ ),
112
+ },
113
+ {
114
+ id: "edit",
115
+ label: "Edit",
116
+ content: (
117
+ <SectionCard title="Edit webhook" description="Changing where events go is confirmed first.">
118
+ <div className="space-y-3">
119
+ <div className="flex flex-col gap-1"><Label htmlFor="wd-url">Endpoint URL</Label><Input id="wd-url" value={form.url} onChange={(e) => setForm({ ...form, url: e.target.value })} /></div>
120
+ {form.url && !urlOk ? <p className="text-xs text-error-foreground">The endpoint must be an https:// URL.</p> : null}
121
+ <div className="flex flex-col gap-1"><Label htmlFor="wd-name">Name</Label><Input id="wd-name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} /></div>
122
+ {Object.entries(byCategory).map(([category, items]) => (
123
+ <fieldset key={category}>
124
+ <legend className="text-xs font-medium uppercase text-muted-foreground">{category}</legend>
125
+ <div className="flex flex-wrap gap-x-4 gap-y-1">
126
+ {items.map((e) => <label key={e.event} className="flex items-center gap-1 text-sm" title={e.description ?? undefined}><input type="checkbox" checked={form.events.includes(e.event)} onChange={() => toggle(e.event)} />{e.event}</label>)}
127
+ </div>
128
+ </fieldset>
129
+ ))}
130
+ <AskButton ask={ask} label="Save changes" variant="default" tier="external" title={`Update the webhook for ${hook.url}`} confirmLabel="Save" disabled={!urlOk || form.events.length === 0}
131
+ description={`Changes where ${brand.linkLabel} sends your data.`} run={() => g8Patch(`/webhooks/${hook.id}`, { url: form.url.trim(), name: form.name.trim() || null, events: form.events })} onDone={changed} />
132
+ </div>
133
+ </SectionCard>
134
+ ),
135
+ },
136
+ {
137
+ id: "deliveries",
138
+ label: "Deliveries",
139
+ content: deliveries.loading && !deliveries.data ? <Skeleton className="h-16 w-full" /> : deliveries.error ? <ApiError error={deliveries.error} onRetry={() => void deliveries.refetch()} /> : rows.length === 0 ? <p className="text-sm text-muted-foreground">No deliveries yet.</p> : (
140
+ <table className="w-full text-sm" aria-label="Deliveries">
141
+ <thead><tr className="text-left text-muted-foreground"><th>Event</th><th>Status</th><th>Code</th><th>Attempts</th><th>When</th></tr></thead>
142
+ <tbody>
143
+ {rows.map((d) => (
144
+ <tr key={d.id} className="border-t" title={d.error_message ?? undefined}>
145
+ <td>{d.event}</td><td>{d.status}</td><td>{d.response_code ?? "-"}</td><td>{d.attempts ?? 1}{d.max_attempts ? ` of ${d.max_attempts}` : ""}</td><td>{formatWhen(d.created_at)}</td>
146
+ </tr>
147
+ ))}
148
+ </tbody>
149
+ </table>
150
+ ),
151
+ },
152
+ ];
153
+ return (
154
+ <>
155
+ <DetailPanel title={hook.name || hook.url} subtitle={hook.name ? hook.url : undefined} badges={<StatusPill tone={active ? "positive" : "neutral"}>{active ? "active" : "paused"}</StatusPill>} actions={extraActions ? <>{actions}{extraActions}</> : actions} tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
156
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
157
+ <ConfirmSpendDialog {...dialog} />
158
+ </>
159
+ );
160
+ }
@@ -27,10 +27,14 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
27
  import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
28
28
  import { EmptyState } from "@/g8/components/empty-state";
29
29
  import { arrayOf, formatWhen } from "../lib/integrations-utils";
30
+ import { plural } from "@/g8/lib/plural";
31
+ import { DEFAULT_BRAND, useBrand } from "@/g8/hooks/use-brand";
30
32
 
31
33
  export interface WebhookManagerProps {
32
34
  /** Pre-fill the endpoint URL of the create form (your /api/webhooks/graph8 route). */
33
35
  defaultUrl?: string;
36
+ /** Called when a person opens a webhook (show a WebhookDetail). */
37
+ onSelectWebhook?: (webhook: { id: string; url: string; name?: string | null }) => void;
34
38
  className?: string;
35
39
  }
36
40
 
@@ -65,13 +69,13 @@ type Pending =
65
69
  | { kind: "rotate"; hook: Webhook }
66
70
  | { kind: "delete"; hook: Webhook };
67
71
 
68
- function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: string; label: string } {
72
+ function copyFor(p: Pending, platform: string = DEFAULT_BRAND.linkLabel): { tier: ConfirmTier; title: string; description: string; label: string } {
69
73
  switch (p.kind) {
70
74
  case "create":
71
75
  return {
72
76
  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.",
77
+ title: `Send ${plural(p.events.length, "event type")} to ${p.url}`,
78
+ description: `${platform} will POST signed events to this URL as they happen.`,
75
79
  label: "Create webhook",
76
80
  };
77
81
  case "toggle":
@@ -80,7 +84,7 @@ function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: s
80
84
  title: `${p.hook.is_active === false ? "Resume" : "Pause"} deliveries to ${p.hook.url}`,
81
85
  description:
82
86
  p.hook.is_active === false
83
- ? "graph8 starts sending events to this URL again."
87
+ ? `${platform} starts sending events to this URL again.`
84
88
  : "Events stop being sent to this URL until you resume.",
85
89
  label: p.hook.is_active === false ? "Resume" : "Pause",
86
90
  };
@@ -133,7 +137,8 @@ function Deliveries({ hook }: { hook: Webhook }) {
133
137
  );
134
138
  }
135
139
 
136
- export function WebhookManager({ defaultUrl = "", className }: WebhookManagerProps) {
140
+ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }: WebhookManagerProps) {
141
+ const brand = useBrand();
137
142
  const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
138
143
  const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
139
144
  const [url, setUrl] = useState(defaultUrl);
@@ -169,7 +174,7 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
169
174
  );
170
175
  const urlOk = /^https:\/\/\S+$/.test(url.trim());
171
176
  const toggle = (event: string) => setChosen((c) => (c.includes(event) ? c.filter((x) => x !== event) : [...c, event]));
172
- const confirm = pending ? copyFor(pending) : null;
177
+ const confirm = pending ? copyFor(pending, brand.linkLabel) : null;
173
178
 
174
179
  return (
175
180
  <div className={`space-y-4 ${className ?? ""}`}>
@@ -178,7 +183,7 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
178
183
  <div className="grid gap-2 md:grid-cols-2">
179
184
  <Input
180
185
  aria-label="Endpoint URL"
181
- placeholder="https://yourapp.com/api/webhooks/graph8"
186
+ placeholder={`https://yourapp.com/api/webhooks/${brand.linkLabel.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`}
182
187
  value={url}
183
188
  onChange={(e) => setUrl(e.target.value)}
184
189
  />
@@ -238,7 +243,7 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
238
243
  <li key={h.id} className="space-y-2 px-3 py-3">
239
244
  <div className="flex flex-wrap items-center justify-between gap-2">
240
245
  <div>
241
- <div className="font-medium">{h.name || h.url}</div>
246
+ {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>}
242
247
  <div className="text-xs text-gray-500">
243
248
  {h.name ? `${h.url} - ` : ""}
244
249
  {(h.events ?? []).length} events - {h.is_active === false ? "paused" : "active"}
@@ -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)}>