@graph8/sdk 0.370.0 → 0.374.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 (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -59,7 +59,7 @@ function UrlLookup() {
59
59
  { enabled: submitted != null },
60
60
  );
61
61
  return (
62
- <section aria-label="URL lookup" className="rounded border border-gray-200 p-3">
62
+ <section aria-label="URL lookup" className="rounded border border-border p-3">
63
63
  <form
64
64
  className="flex gap-2"
65
65
  onSubmit={(e) => {
@@ -76,13 +76,13 @@ function UrlLookup() {
76
76
  {result.error ? <ApiError error={result.error} onRetry={() => void result.refetch()} /> : null}
77
77
  {submitted && !result.loading && !result.error ? (
78
78
  (result.data ?? []).length === 0 ? (
79
- <p className="mt-3 text-sm text-gray-500">No identified companies visited this URL in the last 30 days.</p>
79
+ <p className="mt-3 text-sm text-muted-foreground">No identified companies visited this URL in the last 30 days.</p>
80
80
  ) : (
81
81
  <ul className="mt-3 divide-y text-sm">
82
82
  {(result.data ?? []).map((c) => (
83
83
  <li key={`${c.company_id}-${c.domain}`} className="flex justify-between py-1.5">
84
84
  <span>{c.name ?? c.domain}</span>
85
- <span className="text-gray-500">
85
+ <span className="text-muted-foreground">
86
86
  {c.visit_count} visits, {c.contacts_seen} people
87
87
  </span>
88
88
  </li>
@@ -132,7 +132,7 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
132
132
  </div>
133
133
 
134
134
  {feed.error && status === 403 ? (
135
- <p role="alert" className="rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900">
135
+ <p role="alert" className="rounded border border-warning-foreground/25 bg-warning p-3 text-sm text-warning-foreground">
136
136
  Account intent is not enabled for this workspace. Ask {brand.linkLabel} to switch on ABM intent.
137
137
  </p>
138
138
  ) : feed.error ? (
@@ -167,16 +167,16 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
167
167
  >
168
168
  <TableCell>
169
169
  <div className="font-medium">{a.company_name ?? a.company_domain ?? "Unknown company"}</div>
170
- <div className="text-xs text-gray-500">{a.company_domain}</div>
170
+ <div className="text-xs text-muted-foreground">{a.company_domain}</div>
171
171
  </TableCell>
172
172
  <TableCell>
173
173
  <span className={`rounded px-2 py-0.5 text-xs ${strengthClass(a.account_strength)}`}>{a.account_strength ?? "-"}</span>
174
- {a.is_high_intent ? <span className="ml-1 text-xs font-medium text-red-700">High intent</span> : null}
174
+ {a.is_high_intent ? <span className="ml-1 text-xs font-medium text-error-foreground">High intent</span> : null}
175
175
  </TableCell>
176
176
  <TableCell>
177
177
  {a.total_signal_count ?? 0}
178
178
  {typeof a.trend_pct === "number" ? (
179
- <span className="ml-1 text-xs text-gray-500">
179
+ <span className="ml-1 text-xs text-muted-foreground">
180
180
  ({a.trend_pct > 0 ? "+" : ""}
181
181
  {Math.round(a.trend_pct)}%)
182
182
  </span>
@@ -195,7 +195,7 @@ export function IntentFeed({ days = 30, keywordIds, pageSize = 25, refreshSecond
195
195
  </Table>
196
196
  )}
197
197
 
198
- <div className="flex items-center justify-between text-sm text-gray-600">
198
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
199
199
  <span>{total != null ? `${total.toLocaleString()} accounts` : ""}</span>
200
200
  <div className="flex gap-2">
201
201
  <Button variant="outline" size="sm" disabled={page <= 1 || feed.loading} onClick={() => setPage((p) => p - 1)}>
@@ -185,8 +185,8 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
185
185
  return (
186
186
  <div className={`space-y-4 ${className ?? ""}`}>
187
187
  <div className="grid gap-3 md:grid-cols-2">
188
- <section className="space-y-2 rounded border border-gray-200 p-3">
189
- <h3 className="text-sm font-semibold">Track keywords</h3>
188
+ <section className="space-y-2 rounded border border-border p-3">
189
+ <h3 className="text-sm font-medium">Track keywords</h3>
190
190
  <Textarea
191
191
  aria-label="Keywords"
192
192
  rows={3}
@@ -198,8 +198,8 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
198
198
  Track keywords
199
199
  </Button>
200
200
  </section>
201
- <section className="space-y-2 rounded border border-gray-200 p-3">
202
- <h3 className="text-sm font-semibold">Track a competitor domain</h3>
201
+ <section className="space-y-2 rounded border border-border p-3">
202
+ <h3 className="text-sm font-medium">Track a competitor domain</h3>
203
203
  <Input
204
204
  aria-label="Competitor domain"
205
205
  placeholder="competitor.com"
@@ -228,7 +228,7 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
228
228
  </Button>
229
229
  </div>
230
230
  {domainResult ? (
231
- <p className="text-xs text-gray-600" role="status">
231
+ <p className="text-xs text-muted-foreground" role="status">
232
232
  {domainResult.dry_run ? "Preview: " : "Saved: "}
233
233
  {domainResult.pages_seeded ?? 0} pages, {domainResult.contacts_seeded ?? 0} people, {domainResult.companies_seeded ?? 0}{" "}
234
234
  companies. {domainResult.message}
@@ -301,10 +301,10 @@ export function IntentKeywordManager({ confirmListing = true, pageSize = 50, onS
301
301
  )}
302
302
 
303
303
  {companies ? (
304
- <section className="rounded border border-gray-200 p-3" aria-label="Keyword companies">
305
- <h3 className="text-sm font-semibold">Companies interested in "{companies.keyword}"</h3>
304
+ <section className="rounded border border-border p-3" aria-label="Keyword companies">
305
+ <h3 className="text-sm font-medium">Companies interested in "{companies.keyword}"</h3>
306
306
  {companies.rows.length === 0 ? (
307
- <p className="mt-2 text-sm text-gray-500">No resolved companies yet.</p>
307
+ <p className="mt-2 text-sm text-muted-foreground">No resolved companies yet.</p>
308
308
  ) : (
309
309
  <ul className="mt-2 grid gap-1 text-sm md:grid-cols-2">
310
310
  {companies.rows.map((c, i) => (
@@ -88,8 +88,8 @@ export function formatDate(value: string | null | undefined): string {
88
88
  /** Tailwind classes for an account-strength label. Unknown labels stay neutral. */
89
89
  export function strengthClass(strength: string | null | undefined): string {
90
90
  const s = (strength ?? "").toLowerCase();
91
- if (s.includes("strong") || s === "high") return "bg-green-100 text-green-800";
92
- if (s.includes("moderate") || s === "medium") return "bg-amber-100 text-amber-800";
93
- if (s.includes("weak") || s === "low") return "bg-gray-100 text-gray-700";
94
- return "bg-gray-100 text-gray-700";
91
+ if (s.includes("strong") || s === "high") return "bg-positive text-positive-foreground";
92
+ if (s.includes("moderate") || s === "medium") return "bg-warning text-warning-foreground";
93
+ if (s.includes("weak") || s === "low") return "bg-muted text-foreground";
94
+ return "bg-muted text-foreground";
95
95
  }
@@ -13,6 +13,7 @@ import { useState } from "react";
13
13
  import { Radar } from "lucide-react";
14
14
  import { Button, Skeleton } from "@graph8/sdk/ui";
15
15
  import { ApiError } from "@/g8/components/api-error";
16
+ import { FirstNotice } from "@/g8/components/paging-notices";
16
17
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
17
18
  import { EmptyState } from "@/g8/components/empty-state";
18
19
  import { SectionCard } from "@/g8/components/screen/section-card";
@@ -42,8 +43,11 @@ export function AccountBriefTab({ companyId, name }: { companyId: number; name?:
42
43
  );
43
44
  }
44
45
 
46
+ /** The signals API has no paging: a full read is announced under the list. */
47
+ const SIGNAL_LIMIT = 20;
48
+
45
49
  export function AccountSignalsTab({ companyId }: { companyId: number }) {
46
- const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: 20 }, { signal }), [companyId]);
50
+ const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: SIGNAL_LIMIT }, { signal }), [companyId]);
47
51
  if (q.error) return <ApiError error={q.error} title="Could not load signals" onRetry={() => void q.refetch()} />;
48
52
  if (q.loading && !q.data) return <Skeleton className="h-24 w-full" aria-label="Loading signals" />;
49
53
  const payload = body(q.data);
@@ -51,6 +55,7 @@ export function AccountSignalsTab({ companyId }: { companyId: number }) {
51
55
  const signals = raw.map(asRec);
52
56
  if (signals.length === 0) return <EmptyState icon={<Radar className="size-8" />} title="Quiet" description="No recent buying or activity signals for this account." />;
53
57
  return (
58
+ <>
54
59
  <ul className="divide-y rounded-md border" aria-label="Signals">
55
60
  {signals.map((s, i) => {
56
61
  const when = text(s.detected_at ?? s.created_at ?? s.occurred_at);
@@ -62,5 +67,7 @@ export function AccountSignalsTab({ companyId }: { companyId: number }) {
62
67
  );
63
68
  })}
64
69
  </ul>
70
+ <FirstNotice shown={signals.length} limit={SIGNAL_LIMIT} noun="signals" />
71
+ </>
65
72
  );
66
73
  }
@@ -35,15 +35,18 @@ export interface ChurnListProps {
35
35
  }
36
36
 
37
37
  /** Loaded on click: one call per row up front would be 25 calls for a page the person may only skim. */
38
+ /** The signals API has no paging: a full read is shown as "20+ recent signals". */
39
+ const SIGNAL_LIMIT = 20;
40
+
38
41
  function Intent({ companyId }: { companyId: number }) {
39
42
  const [asked, setAsked] = useState(false);
40
- const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: 20 }, { signal }), [companyId], { enabled: asked });
43
+ const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: SIGNAL_LIMIT }, { signal }), [companyId], { enabled: asked });
41
44
  if (!asked) return <Button size="sm" variant="ghost" aria-label={`Check activity for ${companyId}`} onClick={() => setAsked(true)}>Check</Button>;
42
45
  if (q.loading) return <Skeleton className="h-4 w-16" />;
43
46
  if (q.error) return <span className="text-muted-foreground text-xs">n/a</span>;
44
47
  const payload = body(q.data);
45
48
  const signals = Array.isArray(payload) ? payload : Array.isArray(asRec(payload).signals) ? (asRec(payload).signals as unknown[]) : [];
46
- return <span className="text-xs">{signals.length ? `${signals.length} recent signals` : "Quiet"}</span>;
49
+ return <span className="text-xs">{signals.length ? `${signals.length}${signals.length >= SIGNAL_LIMIT ? "+" : ""} recent signals` : "Quiet"}</span>;
47
50
  }
48
51
 
49
52
  export function ChurnList({ stageSlug = "churned", winBackSlug = "leads", onOpenCompany, pageSize = 25, className }: ChurnListProps) {
@@ -41,7 +41,7 @@ function Summary() {
41
41
  return (
42
42
  <div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4" data-testid="revenue-summary">
43
43
  {tiles.map(([key, value]) => (
44
- <Card key={key}><CardContent className="p-4"><div className="text-2xl font-semibold">{value.toLocaleString()}</div><div className="text-muted-foreground text-xs capitalize">{key.replace(/_count$/, "").replace(/_/g, " ")}</div></CardContent></Card>
44
+ <Card key={key}><CardContent className="p-4"><div className="text-2xl font-medium">{value.toLocaleString()}</div><div className="text-muted-foreground text-xs capitalize">{key.replace(/_count$/, "").replace(/_/g, " ")}</div></CardContent></Card>
45
45
  ))}
46
46
  </div>
47
47
  );
@@ -39,7 +39,7 @@ and `contacts:read/write`. The screens use the foundation screen kit (`component
39
39
  shows `ready_count / warned / blocked` and retries with `conflict_resolution: "skip_all" | "add_all"`.
40
40
  4. **Members**: `ContactTable listId={id}` (or `GET /contacts?list_id=`). Removing uses
41
41
  `DELETE /lists/{list_id}/contacts { contact_ids }`; contacts are never deleted by it.
42
- 5. **Suppress**: `POST /contacts/suppressions/bulk-add { contact_ids, channel, reason }`; read back with
42
+ 5. **Suppress**: pick people by name with the `ContactPicker` (pickers block) (no ids to type; pasting ids stays under "Paste ids instead"), then `POST /contacts/suppressions/bulk-add { contact_ids, channel, reason }`; read back with
43
43
  `GET /contacts/suppressions`. The ledger has ids only (no names or emails): open the contact for details.
44
44
  6. **Reinstate**: `POST /contacts/suppressions/reinstate { contact_ids }`. Contacts who unsubscribed themselves
45
45
  can be refused; the answer lists them in `skipped`.
@@ -107,3 +107,9 @@ Ask the person: who may reinstate suppressed contacts (decide it in the proxy's
107
107
  | checkContactSuppression | GET /contacts/{contact_id}/suppression | read | SuppressionManager |
108
108
  | reinstateSuppressions | POST /contacts/suppressions/reinstate | write | SuppressionManager |
109
109
  | refreshDynamicList, checkListIntegrity | POST /lists/{id}/refresh, /lists/{id}/integrity-check | write, read | recipes |
110
+
111
+ ## Cross-block glue
112
+
113
+ - `AddCompanyPeopleToListButton` (`components/add-company-people-to-list-button.tsx`): "Add people to list" for companies. Lists hold contacts, so it reads the people at the companies (`GET /contacts?company_id=`, up to 500) and opens `AddToListButton` for them. `AddToListButton` gains optional `defaultOpen`, `hideTrigger` and `onClosed`; `ListsScreen` gains optional `onOpenContact` so list members open in your contacts screen.
114
+
115
+ - `RecordListsTab` (`components/record-lists-tab.tsx`): the lists one contact belongs to (`getContactLists`), with `AddToListButton` and a confirmed Remove (`removeContactsFromList`, destructive). `extraDetailTabs` entry for ContactsScreen: `{ id: "lists", label: "Lists", render: (c) => <RecordListsTab contactId={c.id} onOpenList={(id) => router.push(`/lists?id=${id}`)} /> }`.
@@ -12,7 +12,8 @@
12
12
  "finished ListsScreen and SuppressionsScreen pages"
13
13
  ],
14
14
  "dependsOn": [
15
- "crm"
15
+ "crm",
16
+ "pickers"
16
17
  ],
17
18
  "operations": [
18
19
  "add_contacts_to_list_lists__list_id__contacts_post",
@@ -22,6 +23,7 @@
22
23
  "create_list_lists_post",
23
24
  "delete_list_lists__list_id__delete",
24
25
  "download_list_lists__audience_id__download_get",
26
+ "get_contact_lists_contacts__contact_id__lists_get",
25
27
  "list_contact_fields_fields_get",
26
28
  "list_contacts_contacts_get",
27
29
  "list_lists_lists_get",
@@ -86,7 +88,8 @@
86
88
  "list_suppressions_contacts_suppressions_get",
87
89
  "bulk_add_suppressions_contacts_suppressions_bulk_add_post",
88
90
  "check_contact_suppression_contacts__contact_id__suppression_get",
89
- "reinstate_suppressions_contacts_suppressions_reinstate_post"
91
+ "reinstate_suppressions_contacts_suppressions_reinstate_post",
92
+ "list_contacts_contacts_get"
90
93
  ],
91
94
  "tiers": [
92
95
  "read",
@@ -121,12 +124,37 @@
121
124
  "bulk_add_suppressions_contacts_suppressions_bulk_add_post",
122
125
  "check_contact_suppression_contacts__contact_id__suppression_get",
123
126
  "list_suppressions_contacts_suppressions_get",
124
- "reinstate_suppressions_contacts_suppressions_reinstate_post"
127
+ "reinstate_suppressions_contacts_suppressions_reinstate_post",
128
+ "list_contacts_contacts_get"
125
129
  ],
126
130
  "tiers": [
127
131
  "read",
128
132
  "write"
129
133
  ]
134
+ },
135
+ {
136
+ "name": "AddCompanyPeopleToListButton",
137
+ "file": "components/add-company-people-to-list-button.tsx",
138
+ "purpose": "\"Add people to list\" for one or more companies: reads the people at them, then AddToListButton picks the list and adds them.",
139
+ "operations": [
140
+ "list_contacts_contacts_get"
141
+ ],
142
+ "tiers": [
143
+ "read"
144
+ ]
145
+ },
146
+ {
147
+ "name": "RecordListsTab",
148
+ "file": "components/record-lists-tab.tsx",
149
+ "purpose": "The lists one contact belongs to, with Add to list and a confirmed Remove per list; an extraDetailTabs entry for ContactsScreen.",
150
+ "operations": [
151
+ "get_contact_lists_contacts__contact_id__lists_get",
152
+ "remove_contacts_from_list_lists__list_id__contacts_delete"
153
+ ],
154
+ "tiers": [
155
+ "read",
156
+ "destructive"
157
+ ]
130
158
  }
131
159
  ],
132
160
  "hooks": [],
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AddCompanyPeopleToListButton: "Add people to list" for one or more companies. Lists hold contacts, so this
5
+ * reads the people at the companies and hands them to AddToListButton's dialog (list picker, conflict review).
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listContacts GET /contacts?company_id=&page=&limit=100 (read; every page, capped at 500 people)
9
+ * 2. then AddToListButton: listLists / createList, addContactsToList (write, chosen in the dialog)
10
+ *
11
+ * Reading people is free and adds nothing until the person picks a list and confirms in the dialog.
12
+ */
13
+
14
+ import { useState } from "react";
15
+ import { Button } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { g8Get } from "@/g8/lib/g8-client";
18
+ import { AddToListButton } from "./add-to-list-button";
19
+ import type { AddToListResult } from "./add-to-list-button";
20
+
21
+ export interface AddCompanyPeopleToListButtonProps {
22
+ companyIds: number[];
23
+ label?: string;
24
+ variant?: "default" | "outline" | "secondary" | "ghost";
25
+ size?: "default" | "sm";
26
+ disabled?: boolean;
27
+ onAdded?: (result: AddToListResult) => void;
28
+ className?: string;
29
+ }
30
+
31
+ const PEOPLE_CAP = 500;
32
+
33
+ interface PeopleEnvelope { data?: Array<{ id: number }>; pagination?: { has_next?: boolean } }
34
+
35
+ /** Ids of the people at these companies, every page, capped. */
36
+ async function peopleAt(companyIds: number[]): Promise<number[]> {
37
+ const ids = new Set<number>();
38
+ for (const companyId of companyIds) {
39
+ for (let page = 1; page <= 10 && ids.size < PEOPLE_CAP; page++) {
40
+ const env = (await g8Get<Array<{ id: number }>>("/contacts", { company_id: companyId, page, limit: 100 })) as PeopleEnvelope;
41
+ for (const c of env.data ?? []) ids.add(c.id);
42
+ if (!env.pagination?.has_next || (env.data ?? []).length === 0) break;
43
+ }
44
+ }
45
+ return [...ids].slice(0, PEOPLE_CAP);
46
+ }
47
+
48
+ export function AddCompanyPeopleToListButton({ companyIds, label = "Add people to list", variant = "outline", size = "sm", disabled, onAdded, className }: AddCompanyPeopleToListButtonProps) {
49
+ const [loading, setLoading] = useState(false);
50
+ const [error, setError] = useState<unknown>(null);
51
+ const [people, setPeople] = useState<number[] | null>(null);
52
+
53
+ const start = async (): Promise<void> => {
54
+ setLoading(true);
55
+ setError(null);
56
+ try {
57
+ setPeople(await peopleAt(companyIds));
58
+ } catch (e) {
59
+ setError(e);
60
+ } finally {
61
+ setLoading(false);
62
+ }
63
+ };
64
+
65
+ return (
66
+ <>
67
+ <Button className={className} variant={variant} size={size} loading={loading} disabled={disabled || companyIds.length === 0} onClick={() => void start()}>{label}</Button>
68
+ {error ? <ApiError error={error} title="Could not read the people" onRetry={() => void start()} /> : null}
69
+ {people && people.length === 0 ? <p role="status" className="text-xs text-muted-foreground">No people found at {companyIds.length === 1 ? "this company" : "these companies"}.</p> : null}
70
+ {people && people.length > 0 ? <AddToListButton key={people.join(",")} contactIds={people} defaultOpen hideTrigger onAdded={onAdded} onClosed={() => setPeople(null)} /> : null}
71
+ </>
72
+ );
73
+ }
@@ -42,6 +42,12 @@ export interface AddToListButtonProps {
42
42
  disabled?: boolean;
43
43
  onAdded?: (result: AddToListResult) => void;
44
44
  className?: string;
45
+ /** Start with the dialog open (used when another control resolved the contact ids first). */
46
+ defaultOpen?: boolean;
47
+ /** Render the dialog without its trigger button; pair with `defaultOpen` and `onClosed`. */
48
+ hideTrigger?: boolean;
49
+ /** Runs after the dialog closed (cancel, done or Escape). */
50
+ onClosed?: () => void;
45
51
  }
46
52
 
47
53
  interface ConflictDetail { code?: string; ready_count?: number; warned?: unknown[]; blocked?: unknown[]; message?: string }
@@ -52,26 +58,30 @@ function conflictOf(error: unknown): ConflictDetail | null {
52
58
  return e?.status === 409 && e.detail && typeof e.detail === "object" && e.detail.code === "CONFLICT_REVIEW_REQUIRED" ? e.detail : null;
53
59
  }
54
60
 
55
- export function AddToListButton({ contactIds, listId, label = "Add to list", variant = "outline", size = "sm", disabled, onAdded, className }: AddToListButtonProps) {
56
- const [open, setOpen] = useState(false);
61
+ export function AddToListButton({ contactIds, listId, label = "Add to list", variant = "outline", size = "sm", disabled, onAdded, className, defaultOpen = false, hideTrigger = false, onClosed }: AddToListButtonProps) {
62
+ const [open, setOpen] = useState(defaultOpen);
57
63
  const [target, setTarget] = useState<ListSummary | null>(null);
58
64
  const [result, setResult] = useState<AddToListResult | null>(null);
59
65
  const destination = listId ?? target?.id;
60
66
 
61
67
  const add = useG8Mutation(async (resolution?: Resolution) => {
62
- const res = await g8Post<{ added: number; already_member?: number[]; not_found?: number[] }>(`/lists/${destination}/contacts`, {
68
+ const res = await g8Post<{ added?: number; already_member?: number[]; not_found?: number[] }>(`/lists/${destination}/contacts`, {
63
69
  contact_ids: contactIds,
64
70
  ...(resolution ? { conflict_resolution: resolution } : {}),
65
71
  });
66
- return { listId: destination as number, added: res.data.added, already_member: res.data.already_member ?? [], not_found: res.data.not_found ?? [] };
72
+ const already = res.data.already_member ?? [];
73
+ const missing = res.data.not_found ?? [];
74
+ // The response is an open dict: when it carries no count, derive it instead of rendering "undefined".
75
+ const added = typeof res.data.added === "number" ? res.data.added : Math.max(contactIds.length - already.length - missing.length, 0);
76
+ return { listId: destination as number, added, already_member: already, not_found: missing };
67
77
  }, { invalidates: [["lists"], ["contacts", "list"]], onSuccess: (r) => { setResult(r); onAdded?.(r); } });
68
78
 
69
79
  const conflict = conflictOf(add.error);
70
- const close = () => { setOpen(false); setResult(null); setTarget(null); add.reset(); };
80
+ const close = () => { setOpen(false); setResult(null); setTarget(null); add.reset(); onClosed?.(); };
71
81
 
72
82
  return (
73
83
  <>
74
- <Button className={className} variant={variant} size={size} disabled={disabled || contactIds.length === 0} onClick={() => setOpen(true)}>{label}</Button>
84
+ {hideTrigger ? null : <Button className={className} variant={variant} size={size} disabled={disabled || contactIds.length === 0} onClick={() => setOpen(true)}>{label}</Button>}
75
85
  <Dialog open={open} onOpenChange={(o) => (o ? setOpen(true) : close())}>
76
86
  <DialogContent>
77
87
  <DialogHeader>
@@ -89,7 +99,7 @@ export function AddToListButton({ contactIds, listId, label = "Add to list", var
89
99
  <>
90
100
  {listId == null ? <ListPicker value={target?.id} onChange={(l) => { setTarget(l); add.reset(); }} /> : null}
91
101
  {conflict ? (
92
- <div role="alert" className="rounded-md border border-amber-300 bg-amber-50 p-3 text-sm">
102
+ <div role="alert" className="rounded-md border border-warning-foreground/30 bg-warning p-3 text-sm">
93
103
  <p>{conflict.message ?? "Some contacts belong to other reps."}</p>
94
104
  <p className="mt-1">{conflict.ready_count ?? 0} ready, {conflict.warned?.length ?? 0} flagged, {conflict.blocked?.length ?? 0} blocked (never added).</p>
95
105
  <div className="mt-2 flex gap-2">
@@ -16,7 +16,9 @@ import { useEffect, useState } from "react";
16
16
  import { Button, Input } from "@graph8/sdk/ui";
17
17
  import { ApiError } from "@/g8/components/api-error";
18
18
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
19
- import { useG8Query } from "@/g8/hooks/use-g8-query";
19
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
20
+ import { byPage, hasNextOf } from "@/g8/lib/paging";
21
+ import { LoadMore } from "@/g8/components/paging-notices";
20
22
  import { g8Get, g8Post } from "@/g8/lib/g8-client";
21
23
 
22
24
  export interface ListSummary {
@@ -59,17 +61,24 @@ function useDebounced<T>(value: T, ms = 250): T {
59
61
  return v;
60
62
  }
61
63
 
64
+ /** Lists per read; "Load more" appends the next page (the picker used to stop silently at 50). */
65
+ const LIST_PAGE = 50;
66
+
62
67
  export function ListPicker({ value, onChange, type = "contacts", allowCreate = true, suggestedName, className }: ListPickerProps) {
63
68
  const [search, setSearch] = useState("");
64
69
  const [newName, setNewName] = useState(suggestedName ?? "");
65
70
  const q = useDebounced(search.trim());
66
- const lists = useG8Query((signal) => g8Get<ListSummary[]>("/lists", { search: q || undefined, limit: 50 }, { signal }), [q], { key: ["lists", "picker", { q }] });
71
+ const lists = useLoadMoreList(byPage<ListSummary>(LIST_PAGE, async (page, signal) => {
72
+ const env = await g8Get<ListSummary[]>("/lists", { search: q || undefined, page, limit: LIST_PAGE }, { signal });
73
+ const rows = (env.data ?? []) as ListSummary[];
74
+ return { rows, hasNext: hasNextOf(env, LIST_PAGE, rows.length) };
75
+ }), [q]);
67
76
  const create = useG8Mutation((title: string) => g8Post<ListSummary>("/lists", { title, type }), {
68
77
  invalidates: [["lists"]],
69
78
  onSuccess: (res) => { setNewName(""); void lists.refetch(); onChange({ type, ...res.data, title: res.data.title ?? newName }); },
70
79
  });
71
80
 
72
- const rows = ((lists.data?.data ?? []) as ListSummary[]).filter((l) => !l.type || l.type === type);
81
+ const rows = (lists.data ?? []).filter((l) => !l.type || l.type === type);
73
82
  return (
74
83
  <div className={`flex flex-col gap-2 ${className ?? ""}`}>
75
84
  <Input aria-label="Search lists" placeholder="Search lists" value={search} onChange={(e) => setSearch(e.target.value)} />
@@ -86,6 +95,7 @@ export function ListPicker({ value, onChange, type = "contacts", allowCreate = t
86
95
  </li>
87
96
  ))}
88
97
  </ul>
98
+ <LoadMore shown={rows.length} noun="lists" hasMore={lists.hasMore} loading={lists.loadingMore} onLoadMore={lists.loadMore} />
89
99
  {allowCreate ? (
90
100
  <form className="flex items-center gap-2" onSubmit={(e) => { e.preventDefault(); if (newName.trim()) void create.mutate(newName.trim()); }}>
91
101
  <Input aria-label="New list name" placeholder="Or create a new list" value={newName} onChange={(e) => setNewName(e.target.value)} />
@@ -46,7 +46,7 @@ function count(l: ListSummary): number {
46
46
  return l.contact_count ?? l.company_count ?? l.total ?? 0;
47
47
  }
48
48
 
49
- const STATUS_STYLE: Record<string, string> = { failed: "text-destructive", processing: "text-amber-600" };
49
+ const STATUS_STYLE: Record<string, string> = { failed: "text-destructive", processing: "text-warning-foreground" };
50
50
 
51
51
  export function ListTable({ linkParam, pageSize = 25, onOpen, allowCreate = true, refreshKey, className }: ListTableProps) {
52
52
  const [search, setSearch] = useState("");
@@ -67,6 +67,8 @@ export interface ListsScreenProps {
67
67
  extraDetailTabs?: { id: string; label: string; render: (list: ListSummary) => ReactNode }[];
68
68
  /** Where company-list members are managed. Default: the graph8 app. */
69
69
  appUrl?: string;
70
+ /** Open a list member in your contacts screen; without it the member rows are not clickable. */
71
+ onOpenContact?: (contactId: number) => void;
70
72
  className?: string;
71
73
  }
72
74
 
@@ -99,7 +101,7 @@ function when(value?: string | null): string {
99
101
  return value ? new Date(value).toLocaleDateString() : "";
100
102
  }
101
103
 
102
- export function ListsScreen({ pageSize = 25, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, appUrl: appUrlProp, className }: ListsScreenProps) {
104
+ export function ListsScreen({ pageSize = 25, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, appUrl: appUrlProp, onOpenContact, className }: ListsScreenProps) {
103
105
  const brand = useBrand();
104
106
  const appUrl = appUrlProp ?? brand.appUrl;
105
107
  const [search, setSearch] = useState("");
@@ -249,7 +251,7 @@ export function ListsScreen({ pageSize = 25, toolbarActions, renderRowActions, r
249
251
  : (
250
252
  <div className="flex flex-col gap-2">
251
253
  {removeMembers.error ? <ApiError error={removeMembers.error} title="Could not remove them" /> : null}
252
- <ContactTable listId={selected.id} selectable allowDelete={false} pageSize={10} refreshKey={membersKey}
254
+ <ContactTable listId={selected.id} selectable allowDelete={false} pageSize={10} refreshKey={membersKey} onOpen={onOpenContact ? (c) => onOpenContact(c.id) : undefined}
253
255
  renderBulkActions={({ ids }) => <Button size="sm" variant="outline" onClick={() => setRemoving(ids)}>Remove from list</Button>} />
254
256
  </div>
255
257
  ),
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordListsTab: the lists one contact belongs to, with "Add to list" and a confirmed "Remove" per list. Use it as an
5
+ * `extraDetailTabs` entry of ContactsScreen:
6
+ * { id: "lists", label: "Lists", render: (c) => <RecordListsTab contactId={c.id} onOpenList={(id) => router.push(`/lists?id=${id}`)} /> }
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getContactLists GET /contacts/{id}/lists (read; the API answers every list the key can see, unpaged)
10
+ * 2. AddToListButton: listLists / createList, addContactsToList (write, chosen in its dialog)
11
+ * 3. removeContactsFromList DELETE /lists/{list_id}/contacts (destructive: confirmed with ConfirmSpendDialog)
12
+ * Removing takes the contact out of that list only; the contact record and its other lists are untouched.
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import { Button, Skeleton } from "@graph8/sdk/ui";
17
+ import { ListChecks } from "lucide-react";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { g8Delete, g8Get } from "@/g8/lib/g8-client";
24
+ import { AddToListButton } from "./add-to-list-button";
25
+
26
+ /** One row of GET /contacts/{id}/lists. */
27
+ interface MemberList { audience_id?: number | null; audience_title?: string | null }
28
+
29
+ /** The rows of an API answer, whether it came as `{ items }` or a bare array. */
30
+ function itemsOf<T>(env: unknown): T[] {
31
+ const data = (env as { data?: unknown } | null)?.data;
32
+ if (Array.isArray(data)) return data as T[];
33
+ const items = (data as { items?: unknown } | null)?.items;
34
+ return Array.isArray(items) ? (items as T[]) : [];
35
+ }
36
+
37
+ export interface RecordListsTabProps {
38
+ contactId: number;
39
+ /** Route to the list in your lists screen. Without it the rows link to `${listsPath}?id=<list id>`. */
40
+ onOpenList?: (listId: number) => void;
41
+ /** Path of your lists screen for the plain links. Default "/lists". */
42
+ listsPath?: string;
43
+ }
44
+
45
+ export function RecordListsTab({ contactId, onOpenList, listsPath = "/lists" }: RecordListsTabProps) {
46
+ const [removing, setRemoving] = useState<MemberList | null>(null);
47
+ const q = useG8Query(async (signal) => itemsOf<MemberList>(await g8Get(`/contacts/${contactId}/lists`, undefined, { signal })).filter((l) => l.audience_id != null), [contactId]);
48
+ const remove = useG8Mutation(async (list: MemberList) => {
49
+ await g8Delete(`/lists/${list.audience_id}/contacts`, { body: { contact_ids: [contactId] } });
50
+ setRemoving(null);
51
+ await q.refetch();
52
+ }, { invalidates: [["lists"], ["contacts", "list"]], onError: () => setRemoving(null) });
53
+
54
+ const add = <AddToListButton contactIds={[contactId]} onAdded={() => void q.refetch()} />;
55
+ if (q.error) return <ApiError error={q.error} title="Could not load lists" onRetry={() => void q.refetch()} />;
56
+ if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading lists" />;
57
+ return (
58
+ <div className="space-y-3">
59
+ <div className="flex items-center justify-between gap-2">
60
+ <p className="text-sm text-muted-foreground">{q.data.length === 1 ? "In 1 list" : `In ${q.data.length.toLocaleString()} lists`}</p>
61
+ {add}
62
+ </div>
63
+ {remove.error ? <ApiError error={remove.error} title="Could not remove from the list" /> : null}
64
+ {q.data.length === 0 ? (
65
+ <EmptyState compact icon={<ListChecks className="size-8" />} title="Not in any list" description="Add this contact to a list to sequence or sync it with others." />
66
+ ) : (
67
+ <ul className="divide-y rounded-md border" aria-label="Lists">
68
+ {q.data.map((l) => (
69
+ <li key={l.audience_id} className="flex items-center justify-between gap-3 p-3 text-sm">
70
+ <a
71
+ href={`${listsPath}?id=${encodeURIComponent(String(l.audience_id))}`}
72
+ className="min-w-0 truncate font-medium hover:underline"
73
+ onClick={(e) => {
74
+ if (!onOpenList || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
75
+ e.preventDefault();
76
+ onOpenList(l.audience_id as number);
77
+ }}
78
+ >
79
+ {l.audience_title || `List #${l.audience_id}`}
80
+ </a>
81
+ <Button size="sm" variant="ghost" loading={remove.loading && removing?.audience_id === l.audience_id} onClick={() => setRemoving(l)}>Remove</Button>
82
+ </li>
83
+ ))}
84
+ </ul>
85
+ )}
86
+ <ConfirmSpendDialog
87
+ open={removing !== null}
88
+ tier="destructive"
89
+ title={`Remove from ${removing?.audience_title || "this list"}`}
90
+ description="The contact leaves this list only. The contact and its other lists are not changed."
91
+ affected={1}
92
+ confirmLabel="Remove"
93
+ confirming={remove.loading}
94
+ onConfirm={() => removing && void remove.mutate(removing)}
95
+ onCancel={() => setRemoving(null)}
96
+ />
97
+ </div>
98
+ );
99
+ }