@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
@@ -0,0 +1,267 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CampaignsScreen: the finished "Campaigns" page (the AI campaign builder).
5
+ *
6
+ * Composes: CampaignList (browse, open, delete), CampaignWizard (idea -> create -> audience -> steps -> launch),
7
+ * CampaignMetrics, CampaignAssets (documents), CampaignAudienceStep, CampaignSequenceStep, CampaignLaunchStep,
8
+ * IcpEditor (company profile + ICPs) and PersonaEditor.
9
+ *
10
+ * Every record has a complete detail, beside the list or as a full page (`?view=full`): campaign, document
11
+ * (`?doc=`), ICP (IcpDetail) and persona (PersonaDetail).
12
+ *
13
+ * User flows:
14
+ * - Campaigns tab: browse campaigns, open one (deep link `?id=`), delete one (confirmed).
15
+ * - "New campaign" opens the wizard in a sheet: AI ideas (confirmed credit estimate), create, audience, steps
16
+ * and copy, launch (confirmed).
17
+ * - An open campaign has tabs: Details (read and edit name/goal/brief, confirmed), Metrics (7/30/90/365 days),
18
+ * Documents (read, edit, rewrite with AI, add, delete), Audience, Sequence and Launch.
19
+ * - Company profile tab (profile and ICPs) and Personas tab: the context the AI writes from.
20
+ *
21
+ * SDK operations of its own:
22
+ * 1. getCampaign GET /campaigns/{campaign_id} (read; header and Details of the open campaign)
23
+ * 2. updateCampaign PATCH /campaigns/{campaign_id} (external: confirmed; name, goal and brief only)
24
+ * Billable, external and destructive calls in the composed parts open ConfirmSpendDialog there.
25
+ */
26
+
27
+ import { useState } from "react";
28
+ import type { ReactNode } from "react";
29
+ import { Megaphone, Plus } from "lucide-react";
30
+ import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
31
+ import { ApiError } from "@/g8/components/api-error";
32
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
33
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
34
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
35
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
36
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
37
+ import { SplitView } from "@/g8/components/screen/split-view";
38
+ import { StatusPill } from "@/g8/components/screen/status-pill";
39
+ import type { Tone } from "@/g8/components/screen/status-pill";
40
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
41
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
42
+ import { useUrlState } from "@/g8/hooks/use-url-state";
43
+ import { g8Get, g8Patch } from "@/g8/lib/g8-client";
44
+ import { audienceOf, fieldItems, objectOf } from "../lib/campaigns-utils";
45
+ import type { CampaignRow } from "../lib/campaigns-utils";
46
+ import { CampaignAssets } from "./campaign-assets";
47
+ import { CampaignAudienceStep } from "./campaign-audience-step";
48
+ import { CampaignLaunchStep } from "./campaign-launch-step";
49
+ import { CampaignList } from "./campaign-list";
50
+ import { CampaignMetrics } from "./campaign-metrics";
51
+ import { CampaignSequenceStep } from "./campaign-sequence-step";
52
+ import { CampaignWizard } from "./campaign-wizard";
53
+ import { FormField } from "./form-field";
54
+ import { IcpDetail } from "./icp-detail";
55
+ import { IcpEditor } from "./icp-editor";
56
+ import { PersonaDetail } from "./persona-detail";
57
+ import { PersonaEditor } from "./persona-editor";
58
+
59
+ export interface CampaignsScreenProps {
60
+ /** Overrides the website from the saved company profile (needed before a profile exists). */
61
+ websiteUrl?: string;
62
+ /** Extra buttons in the screen header. */
63
+ toolbarActions?: ReactNode;
64
+ /** Extra buttons in the open campaign's header. */
65
+ renderDetailActions?: (campaign: CampaignRow) => ReactNode;
66
+ /** Extra tabs in the open campaign's panel. */
67
+ extraDetailTabs?: { id: string; label: string; render: (campaign: CampaignRow) => ReactNode }[];
68
+ className?: string;
69
+ }
70
+
71
+ const TABS = [
72
+ { id: "campaigns", label: "Campaigns" },
73
+ { id: "profile", label: "Company profile" },
74
+ { id: "personas", label: "Personas" },
75
+ ];
76
+
77
+ /** Campaign status -> pill tone. Unknown states are a warning, never green. */
78
+ export function campaignTone(status: string | null | undefined): Tone {
79
+ const s = (status ?? "").toLowerCase();
80
+ if (["active", "live", "launched", "completed", "complete", "done"].includes(s)) return "positive";
81
+ if (["draft", "pending", "generating", "running", "in_progress", "copy_in_progress", "scheduling"].includes(s)) return "info";
82
+ if (["paused", "archived"].includes(s)) return "neutral";
83
+ return s ? "warning" : "neutral";
84
+ }
85
+
86
+ function Details({ campaign, full, onSaved }: { campaign: CampaignRow; full: unknown; onSaved: () => void }) {
87
+ const [edit, setEdit] = useState<{ name: string; goal: string; brief: string } | null>(null);
88
+ const [confirm, setConfirm] = useState(false);
89
+ const save = useG8Mutation(
90
+ (v: { name: string; goal: string; brief: string }) => g8Patch(`/campaigns/${campaign.id}`, v),
91
+ { onSuccess: () => { setConfirm(false); setEdit(null); onSaved(); }, onError: () => setConfirm(false) },
92
+ );
93
+ const audience = audienceOf(full);
94
+ if (!edit)
95
+ return (
96
+ <div className="space-y-4">
97
+ <KeyValueList
98
+ columns={2}
99
+ items={[
100
+ ...fieldItems(Object.fromEntries(Object.entries(campaign).filter(([k, v]) => k !== "brief" && typeof v !== "object"))),
101
+ { label: "Audience", value: audience.listId ? `${audience.name || `List ${audience.listId}`}${audience.size != null ? ` (${audience.size.toLocaleString()} contacts)` : ""}` : "None attached" },
102
+ ]}
103
+ />
104
+ {campaign.brief ? <p className="whitespace-pre-wrap text-sm">{campaign.brief}</p> : null}
105
+ <Button size="sm" variant="outline" onClick={() => setEdit({ name: campaign.name ?? "", goal: campaign.goal ?? "", brief: campaign.brief ?? "" })}>
106
+ Edit details
107
+ </Button>
108
+ </div>
109
+ );
110
+ return (
111
+ <div className="space-y-3">
112
+ <FormField label="Name" htmlFor="cmp-name"><Input id="cmp-name" value={edit.name} onChange={(e) => setEdit({ ...edit, name: e.target.value })} /></FormField>
113
+ <FormField label="Goal" htmlFor="cmp-goal"><Input id="cmp-goal" value={edit.goal} onChange={(e) => setEdit({ ...edit, goal: e.target.value })} /></FormField>
114
+ <FormField label="Brief" htmlFor="cmp-brief"><Textarea id="cmp-brief" rows={5} value={edit.brief} onChange={(e) => setEdit({ ...edit, brief: e.target.value })} /></FormField>
115
+ {save.error ? <ApiError error={save.error} title="Could not save the campaign" onRetry={save.reset} /> : null}
116
+ <div className="flex gap-2">
117
+ <Button size="sm" disabled={!edit.name.trim()} onClick={() => setConfirm(true)}>Save changes</Button>
118
+ <Button size="sm" variant="ghost" onClick={() => setEdit(null)}>Cancel</Button>
119
+ </div>
120
+ <ConfirmSpendDialog
121
+ open={confirm}
122
+ tier="external"
123
+ title={`Save changes to "${campaign.name ?? "this campaign"}"`}
124
+ description="The campaign details are replaced with what you see. Nobody is contacted."
125
+ confirmLabel="Save changes"
126
+ confirming={save.loading}
127
+ onCancel={() => setConfirm(false)}
128
+ onConfirm={() => void save.mutate({ name: edit.name.trim(), goal: edit.goal, brief: edit.brief })}
129
+ />
130
+ </div>
131
+ );
132
+ }
133
+
134
+ interface DetailProps {
135
+ id: string;
136
+ props: CampaignsScreenProps;
137
+ tab?: string;
138
+ onTabChange: (tab: string) => void;
139
+ docId?: string;
140
+ onDocChange: (id: string | null) => void;
141
+ onClose: () => void;
142
+ onExpand?: () => void;
143
+ onCollapse?: () => void;
144
+ }
145
+
146
+ function Detail({ id, props, tab, onTabChange, docId, onDocChange, onClose, onExpand, onCollapse }: DetailProps) {
147
+ const base = useG8Query((signal) => g8Get(`/campaigns/${id}`, undefined, { signal }), [id]);
148
+ const full = useG8Query((signal) => g8Get(`/campaigns/${id}/full`, undefined, { signal }), [id]);
149
+ const c = { id, ...objectOf(full.data), ...objectOf(base.data) } as CampaignRow;
150
+ const refetch = () => { void base.refetch(); void full.refetch(); };
151
+ if ((base.error as { status?: number } | null)?.status === 404)
152
+ return (
153
+ <div className="space-y-2 p-4 text-sm">
154
+ <p>This campaign no longer exists.</p>
155
+ <Button size="sm" variant="outline" onClick={onClose}>Close</Button>
156
+ </div>
157
+ );
158
+ if (base.error) return <ApiError error={base.error} title="Could not load the campaign" onRetry={() => void base.refetch()} />;
159
+ return (
160
+ <DetailPanel
161
+ title={c.name ?? `Campaign ${id}`}
162
+ subtitle={c.goal ?? undefined}
163
+ badges={c.status ? <StatusPill tone={campaignTone(c.status)} dot>{c.status}</StatusPill> : null}
164
+ actions={props.renderDetailActions?.(c)}
165
+ onClose={onClose}
166
+ onExpand={onExpand}
167
+ onCollapse={onCollapse}
168
+ activeTab={tab}
169
+ onTabChange={onTabChange}
170
+ tabs={[
171
+ {
172
+ id: "details",
173
+ label: "Details",
174
+ // Until the campaign itself has loaded, `c` is only `{ id }`: "Edit details" would seed the form with empty
175
+ // name, goal and brief, and saving would blank the real values. Show a skeleton until the record is here.
176
+ content: base.data === undefined ? (
177
+ <div className="space-y-2" aria-busy="true" data-testid="campaign-details-loading"><Skeleton className="h-24 w-full" /><Skeleton className="h-8 w-32" /></div>
178
+ ) : (
179
+ <Details campaign={c} full={full.data} onSaved={refetch} />
180
+ ),
181
+ },
182
+ { id: "metrics", label: "Metrics", content: <CampaignMetrics campaignId={id} /> },
183
+ { id: "documents", label: "Documents", content: <CampaignAssets campaignId={id} documentId={docId ?? null} onDocumentChange={onDocChange} /> },
184
+ { id: "audience", label: "Audience", content: <CampaignAudienceStep campaignId={id} onAttached={refetch} /> },
185
+ { id: "sequence", label: "Sequence", content: <CampaignSequenceStep campaignId={id} /> },
186
+ { id: "launch", label: "Launch", content: <CampaignLaunchStep campaignId={id} onLaunched={refetch} /> },
187
+ ...(props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(c) })),
188
+ ]}
189
+ />
190
+ );
191
+ }
192
+
193
+ export function CampaignsScreen(props: CampaignsScreenProps) {
194
+ const [tab, setTab] = useUrlState("tab", "campaigns");
195
+ const [selectedId, setSelectedId] = useUrlState("id", "");
196
+ const [detailTab, setDetailTab] = useUrlState("dtab", "");
197
+ const [docId, setDocId] = useUrlState("doc", "");
198
+ const [view, setView] = useUrlState("view", "");
199
+ const [creating, setCreating] = useState(false);
200
+ const [version, setVersion] = useState(0);
201
+ const bump = () => setVersion((v) => v + 1);
202
+ const go = (nextTab: string, id: string) => { setTab(nextTab === "campaigns" ? null : nextTab); setSelectedId(id || null); setDetailTab(null); setDocId(null); setView(null); };
203
+ const close = () => { setSelectedId(null); setDetailTab(null); setDocId(null); setView(null); };
204
+ const create = (
205
+ <Button onClick={() => setCreating(true)}>
206
+ <Plus className="mr-1 h-4 w-4" aria-hidden /> New campaign
207
+ </Button>
208
+ );
209
+ const active = TABS.some((t) => t.id === tab) ? tab : "campaigns";
210
+
211
+ return (
212
+ <ScreenLayout
213
+ className={props.className}
214
+ title="Campaigns"
215
+ description="Plan, write and launch AI-assisted outreach campaigns."
216
+ icon={<Megaphone className="h-5 w-5" aria-hidden />}
217
+ actions={<>{props.toolbarActions}{create}</>}
218
+ tabs={TABS}
219
+ activeTab={active}
220
+ onTabChange={(id) => go(id, "")}
221
+ >
222
+ {active === "profile" ? (
223
+ <SplitView
224
+ expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)}
225
+ detailOpen={Boolean(selectedId)}
226
+ onCloseDetail={close}
227
+ list={<IcpEditor websiteUrl={props.websiteUrl} refreshKey={version} onOpen={(i) => go("profile", i.id)} />}
228
+ detail={selectedId ? <IcpDetail key={selectedId} icpId={selectedId} onExpand={view === "full" ? undefined : () => setView("full")} onCollapse={view === "full" ? () => setView(null) : undefined} onClose={close} onChanged={bump} /> : null}
229
+ />
230
+ ) : active === "personas" ? (
231
+ <SplitView
232
+ expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)}
233
+ detailOpen={Boolean(selectedId)}
234
+ onCloseDetail={close}
235
+ list={<PersonaEditor websiteUrl={props.websiteUrl} refreshKey={version} onOpen={(p) => go("personas", p.id)} />}
236
+ detail={selectedId ? <PersonaDetail key={selectedId} personaId={selectedId} onExpand={view === "full" ? undefined : () => setView("full")} onCollapse={view === "full" ? () => setView(null) : undefined} onClose={close} onChanged={bump} onOpenCampaign={(c) => go("campaigns", c.id)} /> : null}
237
+ />
238
+ ) : (
239
+ <SplitView
240
+ expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)}
241
+ detailOpen={Boolean(selectedId)}
242
+ onCloseDetail={close}
243
+ detailWidth="xl"
244
+ list={<CampaignList linkParam="id" key={version} onOpen={(c) => go("campaigns", c.id)} emptyAction={create} />}
245
+ detail={
246
+ selectedId ? (
247
+ <Detail
248
+ key={selectedId}
249
+ id={selectedId}
250
+ props={props}
251
+ onExpand={view === "full" ? undefined : () => setView("full")} onCollapse={view === "full" ? () => setView(null) : undefined}
252
+ tab={detailTab || undefined}
253
+ onTabChange={(t) => setDetailTab(t === "details" ? null : t)}
254
+ docId={docId || undefined}
255
+ onDocChange={(d) => setDocId(d)}
256
+ onClose={close}
257
+ />
258
+ ) : null
259
+ }
260
+ />
261
+ )}
262
+ <FormSheet width="xl" open={creating} onOpenChange={(o) => { setCreating(o); if (!o) bump(); }} title="New campaign" description="From an idea to a launched campaign in five steps.">
263
+ {creating ? <CampaignWizard websiteUrl={props.websiteUrl} onLaunched={bump} /> : null}
264
+ </FormSheet>
265
+ </ScreenLayout>
266
+ );
267
+ }
@@ -0,0 +1,154 @@
1
+ "use client";
2
+
3
+ /**
4
+ * IcpDetail: one ideal customer profile, complete. Every field (firmographics, buying signals, score,
5
+ * status) and every action: edit, pin or unpin, archive.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listIcps GET /icps (read; the API has no single-ICP read, so the row is found in the list)
9
+ * 2. updateIcp PATCH /icps/{icp_id} (write)
10
+ * 3. pinIcp / unpinIcp POST /icps/{icp_id}/pin|unpin (write)
11
+ * 4. archiveIcp POST /icps/{icp_id}/archive (destructive: confirmed)
12
+ *
13
+ * Related records: the API links no campaign to an ICP, so none are listed.
14
+ */
15
+
16
+ import { useState } from "react";
17
+ import type { ReactNode } from "react";
18
+ import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
22
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
23
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
24
+ import { SectionCard } from "@/g8/components/screen/section-card";
25
+ import { StatusPill } from "@/g8/components/screen/status-pill";
26
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
+ import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
29
+ import { fieldItems, icpFormOf, icpPatch, listOf, text } from "../lib/campaigns-utils";
30
+ import { FormField } from "./form-field";
31
+
32
+ export interface IcpDetailProps {
33
+ icpId: string;
34
+ onClose?: () => void;
35
+ /** Full-page mode: show an "Open full page" (onExpand) or "Back to split view" (onCollapse) button. */
36
+ onExpand?: () => void;
37
+ onCollapse?: () => void;
38
+ /** Called after an edit, pin or archive so a list can refresh. */
39
+ onChanged?: () => void;
40
+ renderActions?: (icp: Record<string, unknown>) => ReactNode;
41
+ className?: string;
42
+ }
43
+
44
+ interface Form { name: string; description: string; industries: string; size: string; regions: string; signals: string }
45
+
46
+ /** The API returns `pinned` (`is_pinned` is accepted too, for older payloads). */
47
+ function isPinned(r: Record<string, unknown> | undefined): boolean {
48
+ return Boolean(r && (r.pinned ?? r.is_pinned));
49
+ }
50
+
51
+ export function IcpDetail({ icpId, onClose, onExpand, onCollapse, onChanged, renderActions, className }: IcpDetailProps) {
52
+ const [form, setForm] = useState<Form | null>(null);
53
+ const [archiving, setArchiving] = useState(false);
54
+ const q = useG8Query((signal) => g8Get("/icps", { limit: 200 }, { signal }), [icpId]);
55
+ const icp = listOf<Record<string, any>>(q.data).find((r) => String(r.id) === icpId);
56
+
57
+ const save = useG8Mutation(async (f: Form) => {
58
+ const patch = icp ? icpPatch(icp, f) : {};
59
+ if (Object.keys(patch).length > 0) await g8Patch(`/icps/${icpId}`, patch); // nothing changed: no request
60
+ setForm(null);
61
+ await q.refetch();
62
+ onChanged?.();
63
+ });
64
+ const pin = useG8Mutation(async () => {
65
+ await g8Post(`/icps/${icpId}/${isPinned(icp) ? "unpin" : "pin"}`, {});
66
+ await q.refetch();
67
+ onChanged?.();
68
+ });
69
+ const archive = useG8Mutation(async () => {
70
+ await g8Post(`/icps/${icpId}/archive`, {});
71
+ setArchiving(false);
72
+ onChanged?.();
73
+ onClose?.();
74
+ }, { onError: () => setArchiving(false) });
75
+
76
+ if (q.error) return <ApiError error={q.error} title="Could not load the ICP" onRetry={() => void q.refetch()} className={className} />;
77
+ if (q.loading && !q.data) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
78
+ if (!icp) return <p className={`p-4 text-sm ${className ?? ""}`}>This ICP no longer exists or was archived.</p>;
79
+
80
+ const f = (icp.firmographics ?? {}) as Record<string, unknown>;
81
+ const arr = (v: unknown) => (Array.isArray(v) ? v.join(", ") : text(v));
82
+ const startEdit = () => {
83
+ save.reset();
84
+ setForm(icpFormOf(icp));
85
+ };
86
+ const set = (k: keyof Form) => (e: { target: { value: string } }) => setForm((cur) => (cur ? { ...cur, [k]: e.target.value } : cur));
87
+ return (
88
+ <DetailPanel
89
+ className={className}
90
+ title={icp.name ?? `ICP ${icpId}`}
91
+ subtitle={icp.description ?? undefined}
92
+ badges={<><StatusPill tone={icp.status && icp.status !== "active" ? "neutral" : "positive"} dot>{icp.status ?? "active"}</StatusPill>{isPinned(icp) ? <StatusPill tone="info">pinned</StatusPill> : null}</>}
93
+ actions={
94
+ <>
95
+ <Button size="sm" variant="outline" onClick={startEdit}>Edit ICP</Button>
96
+ <Button size="sm" variant="outline" loading={pin.loading} onClick={() => void pin.mutate(undefined)}>{isPinned(icp) ? "Unpin ICP" : "Pin ICP"}</Button>
97
+ <Button size="sm" variant="outline" onClick={() => setArchiving(true)}>Archive ICP</Button>
98
+ {renderActions?.(icp)}
99
+ <FormSheet
100
+ open={form !== null}
101
+ onOpenChange={(o) => !o && setForm(null)}
102
+ title="Edit ICP"
103
+ footer={<><Button variant="outline" onClick={() => setForm(null)}>Cancel</Button><Button loading={save.loading} disabled={!form?.name.trim()} onClick={() => form && void save.mutate(form)}>Save ICP</Button></>}
104
+ >
105
+ {form ? (
106
+ <>
107
+ <FormField label="Name" htmlFor="icpd-name"><Input id="icpd-name" value={form.name} onChange={set("name")} /></FormField>
108
+ <FormField label="Description" htmlFor="icpd-desc"><Textarea id="icpd-desc" value={form.description} onChange={set("description")} /></FormField>
109
+ <FormField label="Industries" htmlFor="icpd-ind" hint="Comma separated."><Input id="icpd-ind" value={form.industries} onChange={set("industries")} /></FormField>
110
+ <FormField label="Company size" htmlFor="icpd-size"><Input id="icpd-size" value={form.size} onChange={set("size")} /></FormField>
111
+ <FormField label="Regions" htmlFor="icpd-reg" hint="Comma separated."><Input id="icpd-reg" value={form.regions} onChange={set("regions")} /></FormField>
112
+ <FormField label="Buying signals" htmlFor="icpd-sig" hint="One per line."><Textarea id="icpd-sig" value={form.signals} onChange={set("signals")} /></FormField>
113
+ {save.error ? <ApiError error={save.error} title="Could not save the ICP" /> : null}
114
+ </>
115
+ ) : null}
116
+ </FormSheet>
117
+ <ConfirmSpendDialog
118
+ open={archiving}
119
+ tier="destructive"
120
+ title={`Archive ${icp.name ?? "this ICP"}`}
121
+ description="It drops out of the default list. Campaigns already created from it are unaffected."
122
+ affected={1}
123
+ confirmLabel="Archive ICP"
124
+ confirming={archive.loading}
125
+ onCancel={() => setArchiving(false)}
126
+ onConfirm={() => void archive.mutate(undefined)}
127
+ />
128
+ </>
129
+ }
130
+ onClose={onClose}
131
+ onExpand={onExpand}
132
+ onCollapse={onCollapse}
133
+ tabs={[
134
+ {
135
+ id: "details",
136
+ label: "Details",
137
+ content: (
138
+ <div className="space-y-4">
139
+ {[pin.error, archive.error].map((e, i) => (e ? <ApiError key={i} error={e} /> : null))}
140
+ <KeyValueList columns={2} items={[...fieldItems({ industries: f.industries, company_size: f.company_size, regions: f.regions }), ...fieldItems(Object.fromEntries(Object.entries(icp).filter(([k]) => !["firmographics", "buying_signals", "name", "description"].includes(k))))]} />
141
+ <SectionCard title="Buying signals" description="What tells you an account fits.">
142
+ {((icp.buying_signals ?? []) as unknown[]).length === 0 ? (
143
+ <p className="text-sm text-muted-foreground">No buying signals yet.</p>
144
+ ) : (
145
+ <ul className="list-disc space-y-1 pl-5 text-sm">{((icp.buying_signals ?? []) as unknown[]).map((s, i) => <li key={i}>{text(s)}</li>)}</ul>
146
+ )}
147
+ </SectionCard>
148
+ </div>
149
+ ),
150
+ },
151
+ ]}
152
+ />
153
+ );
154
+ }
@@ -28,13 +28,17 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
28
  import { useJobPoll } from "@/g8/hooks/use-job-poll";
29
29
  import { g8Get, g8Patch, g8Post, g8Put } from "@/g8/lib/g8-client";
30
30
  import { useCompanyProfile } from "../hooks/use-company-profile";
31
- import { CREDIT_ESTIMATES, commaList, lines, listOf, objectOf, settled, statusClass, text } from "../lib/campaigns-utils";
31
+ import { CREDIT_ESTIMATES, commaList, icpFormOf, icpPatch, lines, listOf, objectOf, settled, statusClass, text } from "../lib/campaigns-utils";
32
32
  import { FormField } from "./form-field";
33
33
 
34
34
  export interface IcpEditorProps {
35
35
  /** Overrides the website from the saved profile (needed the first time, before a profile exists). */
36
36
  websiteUrl?: string;
37
37
  onChange?: () => void;
38
+ /** Called when an ICP's name is clicked (a screen opens its detail). Without it the name is plain text. */
39
+ onOpen?: (icp: { id: string; name?: string | null }) => void;
40
+ /** Change it to reload the list (the detail edited or archived an ICP). */
41
+ refreshKey?: number;
38
42
  className?: string;
39
43
  }
40
44
 
@@ -130,19 +134,12 @@ function ProfileCard({ websiteUrl: override, onChange }: IcpEditorProps) {
130
134
  interface IcpForm { name: string; description: string; industries: string; size: string; regions: string; signals: string }
131
135
  const emptyForm: IcpForm = { name: "", description: "", industries: "", size: "", regions: "", signals: "" };
132
136
 
133
- function formOf(icp: IcpRow): IcpForm {
134
- const f = (icp.firmographics ?? {}) as Record<string, unknown>;
135
- const arr = (v: unknown) => (Array.isArray(v) ? v.join(", ") : text(v));
136
- return {
137
- name: icp.name ?? "", description: icp.description ?? "", industries: arr(f.industries), size: text(f.company_size), regions: arr(f.regions),
138
- signals: (icp.buying_signals ?? []).map((s) => text(s)).filter(Boolean).join("\n"),
139
- };
140
- }
137
+ const formOf = (icp: IcpRow): IcpForm => icpFormOf(icp as Record<string, any>);
141
138
 
142
139
  export function IcpEditor(props: IcpEditorProps) {
143
- const { onChange, className } = props;
140
+ const { onChange, onOpen, className } = props;
144
141
  const profile = useCompanyProfile(props.websiteUrl);
145
- const icps = useG8Query((signal) => g8Get("/icps", { limit: 100 }, { signal }), []);
142
+ const icps = useG8Query((signal) => g8Get("/icps", { limit: 100 }, { signal }), [props.refreshKey]);
146
143
  const [editing, setEditing] = useState<IcpRow | "new" | null>(null);
147
144
  const [form, setForm] = useState<IcpForm>(emptyForm);
148
145
  const [archiving, setArchiving] = useState<IcpRow | null>(null);
@@ -151,9 +148,12 @@ export function IcpEditor(props: IcpEditorProps) {
151
148
  const done = () => { void icps.refetch(); onChange?.(); };
152
149
  const saveIcp = useG8Mutation(async () => {
153
150
  const existing = editing && editing !== "new" ? editing : null;
154
- const firmographics = { ...(existing?.firmographics ?? {}), industries: commaList(form.industries), company_size: form.size, regions: commaList(form.regions) };
155
- const body = { name: form.name.trim(), description: form.description, firmographics, buying_signals: lines(form.signals) };
156
- return existing ? g8Patch(`/icps/${existing.id}`, body) : g8Post("/icps", { ...body, website_url: profile.websiteUrl });
151
+ if (existing) {
152
+ const patch = icpPatch(existing as Record<string, any>, form);
153
+ return Object.keys(patch).length > 0 ? g8Patch(`/icps/${existing.id}`, patch) : undefined; // nothing changed: no request
154
+ }
155
+ const firmographics = { industries: commaList(form.industries), company_size: form.size, regions: commaList(form.regions) };
156
+ return g8Post("/icps", { name: form.name.trim(), description: form.description, firmographics, buying_signals: lines(form.signals), website_url: profile.websiteUrl });
157
157
  }, { onSuccess: () => { setEditing(null); done(); } });
158
158
  const archive = useG8Mutation(async (icp: IcpRow) => {
159
159
  setBusyId(icp.id);
@@ -181,7 +181,7 @@ export function IcpEditor(props: IcpEditorProps) {
181
181
  {icps.loading && !icps.data ? <TableRow><TableCell colSpan={4}><Skeleton className="h-6 w-full" /></TableCell></TableRow> : null}
182
182
  {rows.map((icp) => (
183
183
  <TableRow key={icp.id}>
184
- <TableCell className="font-medium">{icp.name}</TableCell>
184
+ <TableCell className="font-medium">{onOpen ? <button type="button" className="text-left hover:underline" aria-label={`Open ${icp.name}`} onClick={() => onOpen(icp)}>{icp.name}</button> : icp.name}</TableCell>
185
185
  <TableCell className="max-w-md truncate text-muted-foreground">{icp.description}</TableCell>
186
186
  <TableCell><span className={`rounded px-2 py-0.5 text-xs ${statusClass(icp.status)}`}>{icp.status ?? "active"}</span></TableCell>
187
187
  <TableCell className="space-x-2 text-right">