@graph8/sdk 0.370.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (466) hide show
  1. package/README.md +19 -0
  2. package/blocks/_foundation/README.md +14 -0
  3. package/blocks/_foundation/block.json +44 -0
  4. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  5. package/blocks/_foundation/components/empty-state.tsx +5 -2
  6. package/blocks/_foundation/components/list-select.tsx +112 -0
  7. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  8. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  9. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  10. package/blocks/_foundation/components/screen/index.ts +1 -0
  11. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  12. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  13. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  14. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  15. package/blocks/_foundation/lib/paging.ts +57 -0
  16. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  17. package/blocks/agents/README.md +81 -4
  18. package/blocks/agents/block.json +311 -2
  19. package/blocks/agents/components/agent-activity.tsx +112 -0
  20. package/blocks/agents/components/agent-approvals.tsx +135 -0
  21. package/blocks/agents/components/agent-builder.tsx +17 -3
  22. package/blocks/agents/components/agent-conversations.tsx +143 -0
  23. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  24. package/blocks/agents/components/agent-operators.tsx +164 -0
  25. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  26. package/blocks/agents/components/agent-runs.tsx +3 -3
  27. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  28. package/blocks/agents/components/agent-test-history.tsx +121 -0
  29. package/blocks/agents/components/agents-screen.tsx +53 -11
  30. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  31. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  32. package/blocks/agents/components/from-template-button.tsx +65 -0
  33. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  34. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  35. package/blocks/agents/components/knowledge-form.tsx +69 -0
  36. package/blocks/agents/components/name-dialog.tsx +32 -0
  37. package/blocks/agents/components/receptionist-button.tsx +46 -0
  38. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  39. package/blocks/agents/components/skill-editor.tsx +147 -0
  40. package/blocks/agents/components/skill-library.tsx +111 -0
  41. package/blocks/agents/lib/agent-types.ts +7 -1
  42. package/blocks/agents/lib/facts.tsx +13 -0
  43. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  44. package/blocks/agents/lib/load-more.tsx +141 -0
  45. package/blocks/agents/lib/skill-utils.ts +91 -0
  46. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  47. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  48. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  49. package/blocks/analytics/components/report-builder.tsx +15 -2
  50. package/blocks/appointments/README.md +6 -0
  51. package/blocks/appointments/block.json +27 -0
  52. package/blocks/appointments/components/booking-page.tsx +2 -2
  53. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  54. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  55. package/blocks/appointments/components/meeting-list.tsx +29 -13
  56. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  57. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  58. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  59. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  60. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  61. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  62. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  63. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  64. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  65. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  66. package/blocks/coach/components/call-scorecard.tsx +7 -7
  67. package/blocks/company-intelligence/README.md +132 -0
  68. package/blocks/company-intelligence/block.json +318 -0
  69. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  70. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  71. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  72. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  73. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  74. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  75. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  76. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  77. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  78. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  79. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  80. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  81. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  82. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  83. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  84. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  85. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  86. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  87. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  88. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  89. package/blocks/crm/README.md +6 -0
  90. package/blocks/crm/block.json +53 -4
  91. package/blocks/crm/components/companies-screen.tsx +23 -9
  92. package/blocks/crm/components/company-detail.tsx +2 -2
  93. package/blocks/crm/components/contact-detail.tsx +4 -4
  94. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  95. package/blocks/crm/components/contacts-screen.tsx +31 -13
  96. package/blocks/crm/components/dedupe-review.tsx +6 -1
  97. package/blocks/crm/components/import-wizard.tsx +3 -7
  98. package/blocks/crm/components/list-select.tsx +20 -0
  99. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  100. package/blocks/crm/components/record-form.tsx +1 -1
  101. package/blocks/crm/components/tasks-screen.tsx +35 -15
  102. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  103. package/blocks/crm/hooks/use-page-query.ts +3 -1
  104. package/blocks/crm/lib/crm-types.ts +14 -0
  105. package/blocks/crm/lib/read-lists.ts +16 -0
  106. package/blocks/deals/README.md +24 -6
  107. package/blocks/deals/block.json +151 -2
  108. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  109. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  110. package/blocks/deals/components/deal-detail.tsx +2 -2
  111. package/blocks/deals/components/deal-filters.tsx +65 -0
  112. package/blocks/deals/components/deal-line-items.tsx +120 -0
  113. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  114. package/blocks/deals/components/deal-related.tsx +30 -9
  115. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  116. package/blocks/deals/components/deals-screen.tsx +57 -22
  117. package/blocks/deals/components/forecast-view.tsx +4 -4
  118. package/blocks/deals/components/pipeline-board.tsx +8 -6
  119. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  120. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  121. package/blocks/deals/components/stage-editor.tsx +1 -1
  122. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  123. package/blocks/deals/lib/deal-extras.ts +156 -0
  124. package/blocks/dialer/README.md +14 -5
  125. package/blocks/dialer/block.json +17 -2
  126. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  127. package/blocks/dialer/components/call-log-screen.tsx +4 -1
  128. package/blocks/dialer/components/call-log.tsx +16 -4
  129. package/blocks/dialer/components/call-queue.tsx +3 -3
  130. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  131. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  132. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  133. package/blocks/dialer/components/session-start-form.tsx +58 -11
  134. package/blocks/dialer/components/softphone.tsx +1 -1
  135. package/blocks/enrichment/README.md +16 -6
  136. package/blocks/enrichment/block.json +19 -7
  137. package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
  138. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  139. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  140. package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
  141. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  142. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  143. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  144. package/blocks/forms/README.md +8 -0
  145. package/blocks/forms/block.json +107 -0
  146. package/blocks/forms/components/form-activity.tsx +71 -0
  147. package/blocks/forms/components/form-backfill.tsx +108 -0
  148. package/blocks/forms/components/form-builder.tsx +1 -1
  149. package/blocks/forms/components/form-embed.tsx +1 -1
  150. package/blocks/forms/components/form-groups.tsx +151 -0
  151. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  152. package/blocks/forms/components/form-route-history.tsx +72 -0
  153. package/blocks/forms/components/form-saved-views.tsx +81 -0
  154. package/blocks/forms/components/forms-screen.tsx +51 -17
  155. package/blocks/forms/components/more-rows.tsx +30 -0
  156. package/blocks/forms/components/submissions-export.tsx +67 -0
  157. package/blocks/forms/components/submissions-table.tsx +15 -6
  158. package/blocks/forms/lib/forms-extra.ts +67 -0
  159. package/blocks/inbox/README.md +5 -0
  160. package/blocks/inbox/block.json +15 -0
  161. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  162. package/blocks/inbox/components/thread-badge.tsx +2 -2
  163. package/blocks/inbox/components/thread-view.tsx +1 -1
  164. package/blocks/integrations/README.md +54 -1
  165. package/blocks/integrations/block.json +189 -15
  166. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  167. package/blocks/integrations/components/connection-health.tsx +59 -0
  168. package/blocks/integrations/components/crm-detail.tsx +20 -1
  169. package/blocks/integrations/components/crm-records.tsx +103 -0
  170. package/blocks/integrations/components/field-mappings.tsx +61 -0
  171. package/blocks/integrations/components/integration-list.tsx +4 -4
  172. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  173. package/blocks/integrations/components/migration-panel.tsx +133 -0
  174. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  175. package/blocks/integrations/components/sync-status.tsx +6 -6
  176. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  177. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  178. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  179. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  180. package/blocks/integrations/lib/crm-utils.ts +39 -0
  181. package/blocks/integrations/lib/facts.tsx +13 -0
  182. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  183. package/blocks/integrations/lib/load-more.tsx +141 -0
  184. package/blocks/intent/components/account-intent-score.tsx +5 -5
  185. package/blocks/intent/components/intent-feed.tsx +8 -8
  186. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  187. package/blocks/intent/lib/intent-utils.ts +4 -4
  188. package/blocks/lifecycle/components/account-related.tsx +8 -1
  189. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  190. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  191. package/blocks/lists/README.md +7 -1
  192. package/blocks/lists/block.json +31 -3
  193. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  194. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  195. package/blocks/lists/components/list-picker.tsx +13 -3
  196. package/blocks/lists/components/list-table.tsx +1 -1
  197. package/blocks/lists/components/lists-screen.tsx +4 -2
  198. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  199. package/blocks/lists/components/suppression-manager.tsx +27 -6
  200. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  201. package/blocks/mailboxes/block.json +4 -0
  202. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  203. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  204. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  205. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  206. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  207. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  208. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  209. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  210. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  211. package/blocks/newsletter/README.md +12 -0
  212. package/blocks/newsletter/block.json +117 -0
  213. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  214. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  215. package/blocks/newsletter/components/more-rows.tsx +30 -0
  216. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  217. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  218. package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
  219. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  220. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  221. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  222. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  223. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  224. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  225. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  226. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  227. package/blocks/nurture/components/nurture-form.tsx +5 -8
  228. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  229. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  230. package/blocks/objects/README.md +1 -1
  231. package/blocks/objects/block.json +13 -5
  232. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  233. package/blocks/objects/components/object-record-detail.tsx +8 -1
  234. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  235. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  236. package/blocks/pages/README.md +14 -0
  237. package/blocks/pages/block.json +113 -1
  238. package/blocks/pages/components/more-rows.tsx +30 -0
  239. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  240. package/blocks/pages/components/page-analytics.tsx +176 -0
  241. package/blocks/pages/components/page-clone.tsx +99 -0
  242. package/blocks/pages/components/page-domains.tsx +167 -0
  243. package/blocks/pages/components/page-editor.tsx +1 -1
  244. package/blocks/pages/components/page-generation-status.tsx +48 -0
  245. package/blocks/pages/components/page-tracking.tsx +137 -0
  246. package/blocks/pages/components/pages-screen.tsx +57 -16
  247. package/blocks/pages/components/seo-report.tsx +6 -1
  248. package/blocks/pages/lib/studio.ts +7 -0
  249. package/blocks/phone-numbers/README.md +74 -0
  250. package/blocks/phone-numbers/block.json +262 -0
  251. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  252. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  253. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  254. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  255. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  256. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  257. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  258. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  259. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  260. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  261. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  262. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  263. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  264. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  265. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  266. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  267. package/blocks/pickers/README.md +23 -0
  268. package/blocks/pickers/block.json +103 -0
  269. package/blocks/pickers/components/company-picker.tsx +90 -0
  270. package/blocks/pickers/components/contact-picker.tsx +131 -0
  271. package/blocks/pickers/components/member-picker.tsx +99 -0
  272. package/blocks/pickers/components/search-picker.tsx +98 -0
  273. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  274. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  275. package/blocks/pickers/lib/use-debounced.ts +13 -0
  276. package/blocks/quotes/README.md +13 -0
  277. package/blocks/quotes/block.json +48 -1
  278. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  279. package/blocks/quotes/components/quote-builder.tsx +1 -1
  280. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  281. package/blocks/quotes/components/quote-list.tsx +5 -3
  282. package/blocks/quotes/components/quote-related.tsx +3 -2
  283. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  284. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  285. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  286. package/blocks/radar/components/radar-overview.tsx +4 -4
  287. package/blocks/radar/components/radar-screen.tsx +2 -2
  288. package/blocks/sequencer/README.md +6 -0
  289. package/blocks/sequencer/block.json +28 -1
  290. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  291. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  292. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  293. package/blocks/sequencer/components/sequence-builder.tsx +19 -20
  294. package/blocks/sequencer/components/sequence-detail.tsx +4 -2
  295. package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
  296. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  297. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  298. package/blocks/sequencer/components/sequences-screen.tsx +3 -0
  299. package/blocks/sequencer/components/step-editor.tsx +11 -11
  300. package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
  301. package/blocks/social-ads/README.md +8 -0
  302. package/blocks/social-ads/block.json +137 -4
  303. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  304. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  305. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  306. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  307. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  308. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  309. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  310. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  311. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  312. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  313. package/blocks/social-ads/components/post-composer.tsx +4 -2
  314. package/blocks/social-ads/components/post-detail.tsx +3 -0
  315. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  316. package/blocks/social-ads/components/social-performance.tsx +80 -0
  317. package/blocks/social-ads/components/social-screen.tsx +33 -6
  318. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  319. package/blocks/surveys/README.md +91 -0
  320. package/blocks/surveys/block.json +299 -0
  321. package/blocks/surveys/components/account-nps.tsx +69 -0
  322. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  323. package/blocks/surveys/components/org-summary.tsx +82 -0
  324. package/blocks/surveys/components/sending-domains.tsx +113 -0
  325. package/blocks/surveys/components/survey-form.tsx +126 -0
  326. package/blocks/surveys/components/survey-responses.tsx +145 -0
  327. package/blocks/surveys/components/survey-results.tsx +72 -0
  328. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  329. package/blocks/surveys/components/survey-send.tsx +162 -0
  330. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  331. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  332. package/blocks/surveys/lib/surveys-types.ts +228 -0
  333. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  334. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  335. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  336. package/blocks/visitors/components/snippet-install.tsx +18 -9
  337. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  338. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  339. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  340. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  341. package/blocks/webchat/components/conversation-list.tsx +1 -1
  342. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  343. package/blocks/workbench/components/run-progress.tsx +1 -1
  344. package/blocks/workbench/components/workbench-grid.tsx +1 -1
  345. package/blocks/workbench/components/workbench-screen.tsx +15 -5
  346. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  347. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  348. package/dist/cli.js +536 -392
  349. package/dist/index.browser.js +1 -1
  350. package/dist/index.browser.js.map +1 -1
  351. package/dist/index.browser.mjs +1 -1
  352. package/dist/index.browser.mjs.map +1 -1
  353. package/dist/index.d.mts +2 -2
  354. package/dist/index.d.ts +2 -2
  355. package/dist/index.js +1 -1
  356. package/dist/index.js.map +1 -1
  357. package/dist/index.mjs +1 -1
  358. package/dist/index.mjs.map +1 -1
  359. package/dist/react.browser.js +1 -1
  360. package/dist/react.browser.js.map +1 -1
  361. package/dist/react.browser.mjs +1 -1
  362. package/dist/react.browser.mjs.map +1 -1
  363. package/dist/react.d.mts +2 -2
  364. package/dist/react.d.ts +2 -2
  365. package/dist/react.js +1 -1
  366. package/dist/react.js.map +1 -1
  367. package/dist/react.mjs +1 -1
  368. package/dist/react.mjs.map +1 -1
  369. package/dist/types/blocks/brand.d.ts +11 -2
  370. package/dist/types/blocks/create.d.ts +7 -3
  371. package/dist/types/blocks/doctor.d.ts +1 -0
  372. package/dist/types/blocks/flows.d.ts +7 -1
  373. package/dist/types/blocks/orgBrand.d.ts +34 -0
  374. package/dist/types/cli.d.ts +3 -0
  375. package/dist/types/version.d.ts +2 -2
  376. package/docs/api/desk.d.ts +2 -2
  377. package/docs/api/desk.md +3 -3
  378. package/package.json +1 -1
  379. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  380. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  381. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  382. package/stacks/6sense.json +17 -4
  383. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  384. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  385. package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
  386. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  387. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  388. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  389. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  390. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  391. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  392. package/stacks/apollo.json +36 -8
  393. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  394. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  395. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  396. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  397. package/stacks/calendly.json +8 -4
  398. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  399. package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
  400. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  401. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  402. package/stacks/clay.json +16 -3
  403. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  404. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  405. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  406. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  407. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  408. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  409. package/stacks/gong.json +23 -6
  410. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  411. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  412. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  413. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  414. package/stacks/hootsuite.json +9 -4
  415. package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
  416. package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
  417. package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
  418. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  419. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  420. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  421. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  422. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  423. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  424. package/stacks/hubspot.json +21 -5
  425. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  426. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  427. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  428. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  429. package/stacks/intercom.json +8 -4
  430. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  431. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  432. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  433. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  434. package/stacks/mailchimp.json +20 -5
  435. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  436. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  437. package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
  438. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  439. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  440. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  441. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  442. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  443. package/stacks/outreach.json +25 -7
  444. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  445. package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
  446. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  447. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  448. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  449. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  450. package/stacks/pipedrive.json +4 -2
  451. package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
  452. package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
  453. package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
  454. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  455. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  456. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  457. package/stacks/salesforce.json +7 -3
  458. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  459. package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
  460. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  461. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  462. package/stacks/zoominfo.json +16 -3
  463. package/styles/styles.css +1 -1
  464. package/templates/next-app/app/globals.css.tmpl +349 -64
  465. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  466. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CreateQuoteFromDeal: a "Create quote from this deal" button. It opens a side sheet with the quote builder for the
5
+ * deal, and after saving the draft shows the quote itself (send, duplicate, lifecycle) in the same sheet.
6
+ *
7
+ * Drop it into the deals screen's Quotes tab: `<DealsScreen renderQuoteActions={(deal) => <CreateQuoteFromDeal dealId={deal.id} />} />`.
8
+ * It cannot live in the deals block because quotes depends on deals, not the other way round.
9
+ *
10
+ * SDK operations (through QuoteBuilder and QuoteViewer): listQuotableProducts, searchQuoteContacts, createQuote and
11
+ * updateQuotePatch (a draft emails nobody, so it saves in one click), then getQuote and the viewer's actions. Sending,
12
+ * editing a sent quote as a draft, deleting and every lifecycle change stay behind ConfirmSpendDialog in QuoteViewer.
13
+ */
14
+ import { useState } from "react";
15
+ import { FileText } from "lucide-react";
16
+ import { Button } from "@graph8/sdk/ui";
17
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
18
+ import { QuoteBuilder } from "./quote-builder";
19
+ import { QuoteViewer } from "./quote-viewer";
20
+
21
+ export interface CreateQuoteFromDealProps {
22
+ dealId: string;
23
+ /** Company (mashup company id) used to find signer contacts. */
24
+ companyId?: number;
25
+ label?: string;
26
+ /** Called with the new quote's id once the draft is saved. */
27
+ onCreated?: (quoteId: string) => void;
28
+ className?: string;
29
+ }
30
+
31
+ export function CreateQuoteFromDeal({ dealId, companyId, label = "Create quote from this deal", onCreated, className }: CreateQuoteFromDealProps) {
32
+ const [open, setOpen] = useState(false);
33
+ const [quoteId, setQuoteId] = useState<string | null>(null);
34
+ const [editing, setEditing] = useState(false);
35
+ const close = (next: boolean) => { setOpen(next); if (!next) { setQuoteId(null); setEditing(false); } };
36
+ return (
37
+ <>
38
+ <Button className={className} size="sm" onClick={() => setOpen(true)}><FileText className="size-4" aria-hidden />{label}</Button>
39
+ <FormSheet open={open} onOpenChange={close} title={quoteId ? "Quote" : "New quote"} description={quoteId ? undefined : "The quote is attached to this deal."} width="xl">
40
+ {open ? (quoteId && editing
41
+ ? <QuoteBuilder key={`edit-${quoteId}`} quoteId={quoteId} onSaved={(id) => { setQuoteId(id); setEditing(false); }} />
42
+ : quoteId
43
+ ? <QuoteViewer key={quoteId} quoteId={quoteId} onEdit={() => setEditing(true)} onOpenQuote={setQuoteId} onDeleted={() => close(false)} />
44
+ : <QuoteBuilder key={dealId} dealId={dealId} companyId={companyId} onSaved={(id) => { setQuoteId(id); onCreated?.(id); }} />) : null}
45
+ </FormSheet>
46
+ </>
47
+ );
48
+ }
@@ -178,7 +178,7 @@ export function QuoteBuilder({ quoteId, dealId, companyId, currency = "USD", onS
178
178
  <div className="text-right text-sm" data-testid="totals">
179
179
  <div>Subtotal {money(sub, form.currency)}</div>
180
180
  <div>Tax {money(taxCents, form.currency)}</div>
181
- <div className="font-semibold">Total {money(sub + taxCents, form.currency)}</div>
181
+ <div className="font-medium">Total {money(sub + taxCents, form.currency)}</div>
182
182
  </div>
183
183
  <details>
184
184
  <summary className="cursor-pointer text-sm font-medium">Add a product</summary>
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuoteLifecycleActions: what you can do with a quote after it exists: download the PDF, archive or restore it, and
5
+ * for a quote that is out for signature, expire it, revoke it or mark it accepted. QuoteViewer renders it under the
6
+ * header; it can also be mounted on its own for a quote you already loaded.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. GET /quotes/{id}/pdf (quotes.getQuotePdf) read - the document as the customer sees it
10
+ * 2. POST /quotes/{id}/archive (quotes.archiveQuote) DESTRUCTIVE - off the active list, reversible -> ConfirmSpendDialog
11
+ * POST /quotes/{id}/unarchive (quotes.unarchiveQuote) external -> ConfirmSpendDialog
12
+ * 3. POST /quotes/{id}/expire (quotes.expireQuote) DESTRUCTIVE - terminal, the customer's link stops working -> ConfirmSpendDialog
13
+ * 4. POST /quotes/{id}/revoke (quotes.revokeQuote) DESTRUCTIVE - terminal, withdraws a sent quote -> ConfirmSpendDialog
14
+ * 5. POST /quotes/{id}/mark-accepted { evidence_type, evidence_note } (quotes.markQuoteAccepted) external - RAISES AN INVOICE -> ConfirmSpendDialog
15
+ * Status rules (the API enforces them too): expire, revoke and mark accepted apply to sent or viewed quotes; archive
16
+ * to any quote that is not archived. Marking accepted runs the same provisioning as a signed envelope, so use it only
17
+ * when the customer paid or signed outside the product; the evidence is stored on the audit trail.
18
+ * The PDF endpoint answers JSON: a link (`url`, `pdf_url`, `download_url`) opens in a new tab, base64 content downloads.
19
+ */
20
+ import { useState } from "react";
21
+ import { Download } from "lucide-react";
22
+ import { Button, Textarea } from "@graph8/sdk/ui";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
26
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
+ import { asRec, text } from "../lib/quotes";
28
+
29
+ export interface QuoteLifecycleActionsProps {
30
+ quoteId: string;
31
+ /** The quote's status: draft, sent, viewed, accepted, declined, voided, expired or archived. */
32
+ status: string;
33
+ /** True when the quote is archived (a status of "archived", or an archived flag on the record). */
34
+ archived?: boolean;
35
+ /** Shown in the dialogs and used for the PDF file name. */
36
+ title?: string;
37
+ quoteNumber?: string;
38
+ /** Called after any lifecycle change, so the caller reloads the quote. */
39
+ onChanged?: () => void;
40
+ className?: string;
41
+ }
42
+
43
+ type Pending = "archive" | "unarchive" | "expire" | "revoke" | "accept";
44
+
45
+ export const EVIDENCE_TYPES: Array<[string, string]> = [
46
+ ["offline_signature", "Signed offline"],
47
+ ["customer_email", "Customer confirmed by email"],
48
+ ["verbal_authorization", "Verbal authorization"],
49
+ ["payment_received", "Payment received"],
50
+ ];
51
+
52
+ function base64Bytes(b64: string): Uint8Array {
53
+ const bin = atob(b64.replace(/^data:application\/pdf;base64,/, "").replace(/\s/g, ""));
54
+ return Uint8Array.from(bin, (c) => c.charCodeAt(0));
55
+ }
56
+
57
+ /** Open or save the PDF an `/pdf` response describes. Throws when it carries neither a link nor content. */
58
+ export function deliverPdf(res: unknown, filename: string): void {
59
+ const raw = asRec(res);
60
+ const data = "data" in raw ? raw.data : raw;
61
+ const rec = asRec(data);
62
+ const link = typeof data === "string" && /^https?:/.test(data) ? data : text(rec.pdf_url ?? rec.download_url ?? rec.signed_url ?? rec.url ?? rec.link);
63
+ if (link) {
64
+ window.open(link, "_blank", "noopener,noreferrer");
65
+ return;
66
+ }
67
+ const b64 = typeof data === "string" ? data : text(rec.pdf_base64 ?? rec.content_base64 ?? rec.base64 ?? rec.pdf ?? rec.content);
68
+ if (!b64) throw new Error("The API returned no PDF for this quote (no link and no file content).");
69
+ const url = URL.createObjectURL(new Blob([base64Bytes(b64) as BlobPart], { type: "application/pdf" }));
70
+ const a = document.createElement("a");
71
+ a.href = url;
72
+ a.download = filename;
73
+ a.click();
74
+ setTimeout(() => URL.revokeObjectURL(url), 10_000);
75
+ }
76
+
77
+ export function QuoteLifecycleActions({ quoteId, status, archived = false, title, quoteNumber, onChanged, className }: QuoteLifecycleActionsProps) {
78
+ const base = `/quotes/${encodeURIComponent(quoteId)}`;
79
+ const [pending, setPending] = useState<Pending | null>(null);
80
+ const [evidenceType, setEvidenceType] = useState(EVIDENCE_TYPES[0][0]);
81
+ const [evidenceNote, setEvidenceNote] = useState("");
82
+ const [needNote, setNeedNote] = useState(false);
83
+ const name = title || "this quote";
84
+ const out = status === "sent" || status === "viewed";
85
+ const isArchived = archived || status === "archived";
86
+
87
+ const pdf = useG8Mutation(async () => { deliverPdf(await g8Get(`${base}/pdf`), `${quoteNumber || "quote"}.pdf`); });
88
+ const run = useG8Mutation(async (kind: Pending) => {
89
+ if (kind === "accept") return g8Post(`${base}/mark-accepted`, { evidence_type: evidenceType, evidence_note: evidenceNote.trim() });
90
+ return g8Post(`${base}/${kind}`);
91
+ }, {
92
+ invalidates: [["quotes"]],
93
+ onSuccess: () => { setPending(null); setEvidenceNote(""); setNeedNote(false); onChanged?.(); },
94
+ });
95
+ const cancel = () => { setPending(null); setNeedNote(false); };
96
+ const confirm = (kind: Pending) => {
97
+ if (kind === "accept" && !evidenceNote.trim()) { setNeedNote(true); return; }
98
+ void run.mutate(kind);
99
+ };
100
+
101
+ return (
102
+ <div className={className}>
103
+ <div className="flex flex-wrap items-center gap-2" role="group" aria-label="Quote lifecycle">
104
+ {isArchived ? <span className="text-xs text-muted-foreground">Archived</span> : null}
105
+ <Button size="sm" variant="outline" loading={pdf.loading} onClick={() => void pdf.mutate(undefined)}><Download className="size-4" aria-hidden />Download PDF</Button>
106
+ {out ? <Button size="sm" variant="outline" onClick={() => setPending("accept")}>Mark accepted</Button> : null}
107
+ {out ? <Button size="sm" variant="ghost" onClick={() => setPending("revoke")}>Revoke</Button> : null}
108
+ {out ? <Button size="sm" variant="ghost" onClick={() => setPending("expire")}>Expire</Button> : null}
109
+ {isArchived ? <Button size="sm" variant="ghost" onClick={() => setPending("unarchive")}>Unarchive</Button> : <Button size="sm" variant="ghost" onClick={() => setPending("archive")}>Archive</Button>}
110
+ </div>
111
+ {pdf.error ? <ApiError error={pdf.error} title="Could not get the PDF" /> : null}
112
+ {run.error ? <ApiError error={run.error} title="That quote action failed" /> : null}
113
+
114
+ {pending === "archive" ? (
115
+ <ConfirmSpendDialog open tier="destructive" title={`Archive "${name}"?`} confirmLabel="Archive" affected={1} confirming={run.loading}
116
+ description="The quote leaves the active list. Nothing is sent and you can unarchive it later." onConfirm={() => confirm("archive")} onCancel={cancel} />
117
+ ) : null}
118
+ {pending === "unarchive" ? (
119
+ <ConfirmSpendDialog open tier="external" title={`Unarchive "${name}"?`} confirmLabel="Unarchive" confirming={run.loading}
120
+ description="The quote returns to the active list." onConfirm={() => confirm("unarchive")} onCancel={cancel} />
121
+ ) : null}
122
+ {pending === "expire" ? (
123
+ <ConfirmSpendDialog open tier="destructive" title={`Expire "${name}"?`} confirmLabel="Expire quote" affected={1} confirming={run.loading}
124
+ description="The customer's link stops working. This is final: create a new quote to offer it again." onConfirm={() => confirm("expire")} onCancel={cancel} />
125
+ ) : null}
126
+ {pending === "revoke" ? (
127
+ <ConfirmSpendDialog open tier="destructive" title={`Revoke "${name}"?`} confirmLabel="Revoke quote" affected={1} confirming={run.loading}
128
+ description="Withdraws the quote you already sent, so the customer can no longer sign it. This is final: use it when the offer itself was wrong." onConfirm={() => confirm("revoke")} onCancel={cancel} />
129
+ ) : null}
130
+ {pending === "accept" ? (
131
+ <ConfirmSpendDialog open tier="external" title={`Mark "${name}" accepted?`} confirmLabel="Mark accepted" recipients={1} confirming={run.loading}
132
+ description={(
133
+ <div className="space-y-2 text-left">
134
+ <p>Accepts the quote on the customer&apos;s behalf. This raises an invoice or subscription with your billing provider, the same as a signed quote. Use it only when the customer paid or signed outside the product.</p>
135
+ <select aria-label="Evidence type" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={evidenceType} onChange={(e) => setEvidenceType(e.target.value)}>
136
+ {EVIDENCE_TYPES.map(([v, label]) => <option key={v} value={v}>{label}</option>)}
137
+ </select>
138
+ <Textarea aria-label="Evidence note" rows={3} placeholder="Describe the evidence (required), for example the email date or payment reference" value={evidenceNote} onChange={(e) => { setEvidenceNote(e.target.value); setNeedNote(false); }} />
139
+ {needNote ? <p role="alert" className="text-sm text-destructive">Add a note describing the evidence before accepting.</p> : null}
140
+ </div>
141
+ )}
142
+ onConfirm={() => confirm("accept")} onCancel={cancel} />
143
+ ) : null}
144
+ </div>
145
+ );
146
+ }
@@ -4,7 +4,7 @@
4
4
  * QuoteList: quotes with a status filter, totals in the quote's own currency, and paging.
5
5
  *
6
6
  * SDK operations, in the order the screen uses them:
7
- * 1. listQuotes GET /quotes?status&mashup_company_id&deal_id&page&limit (read; `{ items, total, page, limit }`)
7
+ * 1. listQuotes GET /quotes?status&include_archived&mashup_company_id&deal_id&page&limit (read; `{ items, total, page, limit }`)
8
8
  *
9
9
  * Read-only: no billable, external or destructive call, so no confirmation. Pass `dealId` or `companyId` to show the
10
10
  * quotes of one deal or company (for a tab on a deal page). `onOpen` receives the quote id for QuoteViewer.
@@ -36,10 +36,11 @@ const STATUSES = ["", "draft", "sent", "viewed", "accepted", "declined", "voided
36
36
 
37
37
  export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, refreshKey, selectedId, className }: QuoteListProps) {
38
38
  const [status, setStatus] = useState("");
39
+ const [archived, setArchived] = useState(false);
39
40
  const [page, setPage] = useState(1);
40
41
  const q = useG8Query(
41
- (signal) => g8Get("/quotes", { status: status || undefined, deal_id: dealId, mashup_company_id: companyId, page, limit: pageSize }, { signal }),
42
- [status, dealId, companyId, page, pageSize, refreshKey],
42
+ (signal) => g8Get("/quotes", { status: status || undefined, include_archived: archived || undefined, deal_id: dealId, mashup_company_id: companyId, page, limit: pageSize }, { signal }),
43
+ [status, archived, dealId, companyId, page, pageSize, refreshKey],
43
44
  );
44
45
  const data = body(q.data);
45
46
  const rows = (Array.isArray(data.items) ? data.items : []) as Rec[];
@@ -51,6 +52,7 @@ export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, ref
51
52
  <select aria-label="Status" value={status} onChange={(e) => { setStatus(e.target.value); setPage(1); }} className="border-input bg-background h-9 rounded-md border px-2 text-sm">
52
53
  {STATUSES.map((s) => <option key={s} value={s}>{s ? statusLabel(s) : "All statuses"}</option>)}
53
54
  </select>
55
+ <label className="flex items-center gap-1 text-sm text-muted-foreground"><input type="checkbox" checked={archived} onChange={(e) => { setArchived(e.target.checked); setPage(1); }} />Include archived</label>
54
56
  {onNew ? <Button className="ml-auto" onClick={onNew}>New quote</Button> : null}
55
57
  </div>
56
58
  {q.error ? <ApiError error={q.error} onRetry={() => void q.refetch()} /> : null}
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * ProductCatalog: every quotable product (Stripe and manual) as a list, with a full product record (all fields and
7
7
  * every price) in a detail panel, and a "Start a quote" action.
8
- * QuoteDealTab: the deal a quote belongs to, as a tab of the quote's detail.
8
+ * QuoteDealTab: the deal a quote belongs to, as a tab of the quote's detail, with "Create another quote" (CreateQuoteFromDeal).
9
9
  *
10
10
  * SDK operations, in order:
11
11
  * ProductCatalog: GET /quotable-products?page&limit (quotes.listQuotableProducts) read
@@ -28,6 +28,7 @@ import { g8Get } from "@/g8/lib/g8-client";
28
28
  import { useG8Query } from "@/g8/hooks/use-g8-query";
29
29
  import { formatMoney, type Deal } from "@/g8/deals/lib/deals-types";
30
30
  import { rowActivationProps } from "@/g8/hooks/use-row-activation";
31
+ import { CreateQuoteFromDeal } from "./create-quote-from-deal";
31
32
  import { body, frequencyOf, money, text, type QuotableProduct, type ProductPrice } from "../lib/quotes";
32
33
  import { useBrand } from "@/g8/hooks/use-brand";
33
34
 
@@ -113,7 +114,7 @@ export function QuoteDealTab({ quoteId, onOpenDeal }: { quoteId: string; onOpenD
113
114
  const d = q.data;
114
115
  if (!d) return <EmptyState title="No deal on this quote" />;
115
116
  return (
116
- <SectionCard title={d.name || "Deal"} actions={onOpenDeal ? <Button size="sm" variant="outline" onClick={() => onOpenDeal(d.id)}>Open deal</Button> : undefined}>
117
+ <SectionCard title={d.name || "Deal"} actions={<>{onOpenDeal ? <Button size="sm" variant="outline" onClick={() => onOpenDeal(d.id)}>Open deal</Button> : null}<CreateQuoteFromDeal dealId={d.id} label="Create another quote" /></>}>
117
118
  <KeyValueList columns={2} items={[
118
119
  { label: "Stage", value: d.stage_name }, { label: "Amount", value: d.amount == null ? null : formatMoney(d.amount, d.currency) }, { label: "Owner", value: d.owner_name ?? d.owner_email },
119
120
  { label: "Close date", value: d.close_date?.slice(0, 10) }, { label: "Pipeline", value: d.pipeline_name }, { label: "Contacts", value: d.contact_count },
@@ -11,6 +11,9 @@
11
11
  * 4. duplicateQuote POST /quotes/{id}/duplicate (external tier, but only copies into a new draft)
12
12
  * 5. deleteQuote DELETE /quotes/{id} (destructive; drafts only)
13
13
  *
14
+ * The lifecycle row (QuoteLifecycleActions): download the PDF, archive or unarchive, and for a sent quote expire, revoke or
15
+ * mark accepted. Those operations live in that component and each risky one is confirmed there.
16
+ *
14
17
  * Send, edit-as-draft and delete go through ConfirmSpendDialog. Duplicate creates an unsent draft and emails
15
18
  * nobody, so it runs on one click (same reasoning as saving a draft in QuoteBuilder).
16
19
  * Send needs a signer on the quote; without one the button explains what is missing instead of failing.
@@ -24,6 +27,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
27
  import { ApiError } from "@/g8/components/api-error";
25
28
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
26
29
  import { asRec, body, lineTotal, linesFromQuote, money, statusLabel, text } from "../lib/quotes";
30
+ import { QuoteLifecycleActions } from "./quote-lifecycle-actions";
27
31
  import { SignatureStatus } from "./signature-status";
28
32
 
29
33
  export interface QuoteViewerProps {
@@ -75,7 +79,7 @@ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className
75
79
  return (
76
80
  <div className={className}>
77
81
  <div className="mb-4 flex flex-wrap items-center gap-2">
78
- <h2 className="text-lg font-semibold">{text(d.title, "Untitled quote")}</h2>
82
+ <h2 className="text-lg font-medium">{text(d.title, "Untitled quote")}</h2>
79
83
  <span className="text-muted-foreground text-sm" data-testid="quote-status">{statusLabel(status)} · {text(d.quote_number)}</span>
80
84
  <div className="ml-auto flex flex-wrap gap-2">
81
85
  {draft ? <Button variant="outline" onClick={() => onEdit?.(quoteId)}>Edit</Button> : null}
@@ -85,6 +89,7 @@ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className
85
89
  {draft || sent ? <Button disabled={!hasSigner} title={hasSigner ? undefined : "Add a signer in the builder first"} onClick={() => setPending("send")}>{sent ? "Resend" : "Send for signature"}</Button> : null}
86
90
  </div>
87
91
  </div>
92
+ <QuoteLifecycleActions className="mb-4" quoteId={quoteId} status={status} archived={Boolean(d.archived_at || d.is_archived || d.archived === true)} title={text(d.title)} quoteNumber={text(d.quote_number)} onChanged={() => void q.refetch()} />
88
93
  {run.error ? <ApiError error={run.error} title="That action failed" /> : null}
89
94
  {copy.error ? <ApiError error={copy.error} title="Could not duplicate" /> : null}
90
95
  {draft && !hasSigner ? <p className="text-muted-foreground mb-3 text-sm">Add a signer before sending.</p> : null}
@@ -104,7 +109,7 @@ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className
104
109
  <div>Subtotal {money(d.subtotal, currency)}</div>
105
110
  {Number(d.discount_amount) ? <div>Discount -{money(d.discount_amount, currency)}</div> : null}
106
111
  <div>Tax {money(d.tax_amount, currency)}</div>
107
- <div className="font-semibold">Total {money(d.total, currency)}</div>
112
+ <div className="font-medium">Total {money(d.total, currency)}</div>
108
113
  </div>
109
114
  </CardContent>
110
115
  </Card>
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordQuotesTab: the quotes tied to one contact (as the signer) or one company, each opening in the quotes screen. Use it
5
+ * as an `extraDetailTabs` entry of ContactsScreen / CompaniesScreen:
6
+ * { id: "quotes", label: "Quotes", render: (c) => <RecordQuotesTab kind="contacts" id={c.id} onOpenQuote={(id) => router.push(`/quotes?id=${id}`)} /> }
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getContactQuotes GET /contacts/{id}/quotes (read) when kind="contacts"
10
+ * getCompanyQuotes GET /companies/{id}/quotes (read) when kind="companies"
11
+ * Both answer every matching quote in one read (the API does not page them). Nothing here is billable, external or
12
+ * destructive; creating and sending a quote happens in the quotes screen.
13
+ */
14
+
15
+ import { Skeleton } from "@graph8/sdk/ui";
16
+ import { FileText } from "lucide-react";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { StatusPill } from "@/g8/components/screen/status-pill";
20
+ import type { Tone } from "@/g8/components/screen/status-pill";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { g8Get } from "@/g8/lib/g8-client";
23
+ import { asRec, linesFromQuote, money, statusLabel, subtotal, text } from "../lib/quotes";
24
+ import type { Rec } from "../lib/quotes";
25
+
26
+ const TONE: Record<string, Tone> = { sent: "info", viewed: "info", accepted: "positive", declined: "negative", voided: "negative", expired: "warning", draft: "neutral", archived: "neutral" };
27
+
28
+ /** The quote's total: the API's own `total` when it sends one, else the sum of its lines. */
29
+ function totalOf(q: Rec): string {
30
+ const currency = text(q.currency, "USD");
31
+ if (typeof q.total === "number") return money(q.total, currency);
32
+ try {
33
+ const sum = subtotal(linesFromQuote(q.line_items));
34
+ return sum > 0 ? money(sum, currency) : "";
35
+ } catch {
36
+ return "";
37
+ }
38
+ }
39
+
40
+ export interface RecordQuotesTabProps {
41
+ kind: "contacts" | "companies";
42
+ id: number;
43
+ /** Route to the quote in your quotes screen. Without it the rows link to `${quotesPath}?id=<quote id>`. */
44
+ onOpenQuote?: (quoteId: string) => void;
45
+ /** Path of your quotes screen for the plain links. Default "/quotes". */
46
+ quotesPath?: string;
47
+ }
48
+
49
+ export function RecordQuotesTab({ kind, id, onOpenQuote, quotesPath = "/quotes" }: RecordQuotesTabProps) {
50
+ const q = useG8Query(async (signal) => {
51
+ const env = asRec(await g8Get(`/${kind}/${id}/quotes`, undefined, { signal }));
52
+ const inner = env.data;
53
+ const rows = Array.isArray(inner) ? inner : (asRec(inner).items ?? []);
54
+ return (rows as unknown[]).map(asRec).filter((r) => text(r.id) !== "");
55
+ }, [kind, id]);
56
+ if (q.error) return <ApiError error={q.error} title="Could not load quotes" onRetry={() => void q.refetch()} />;
57
+ if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading quotes" />;
58
+ if (q.data.length === 0) return <EmptyState compact icon={<FileText className="size-8" />} title="No quotes yet" description={kind === "contacts" ? "Quotes this contact signs show up here." : "Quotes for this company show up here."} />;
59
+ return (
60
+ <ul className="divide-y rounded-md border" aria-label="Quotes">
61
+ {q.data.map((r) => {
62
+ const quoteId = text(r.id);
63
+ const status = text(r.status);
64
+ const total = totalOf(r);
65
+ return (
66
+ <li key={quoteId}>
67
+ <a
68
+ href={`${quotesPath}?id=${encodeURIComponent(quoteId)}`}
69
+ className="flex items-center justify-between gap-3 p-3 text-sm hover:bg-muted"
70
+ onClick={(e) => {
71
+ if (!onOpenQuote || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
72
+ e.preventDefault();
73
+ onOpenQuote(quoteId);
74
+ }}
75
+ >
76
+ <span className="min-w-0">
77
+ <span className="block truncate font-medium">{text(r.title, "Untitled quote")}</span>
78
+ {text(r.quote_number) ? <span className="block text-xs text-muted-foreground">{text(r.quote_number)}</span> : null}
79
+ </span>
80
+ <span className="flex shrink-0 items-center gap-2">
81
+ {total ? <span className="tabular-nums text-muted-foreground">{total}</span> : null}
82
+ <StatusPill tone={TONE[status] ?? "neutral"} dot>{statusLabel(status)}</StatusPill>
83
+ </span>
84
+ </a>
85
+ </li>
86
+ );
87
+ })}
88
+ </ul>
89
+ );
90
+ }
@@ -34,7 +34,7 @@ export function CompetitorBattleCard({ competitorId, className }: CompetitorBatt
34
34
  if (!card) return <Skeleton className="h-40 w-full" />;
35
35
  return (
36
36
  <article className={`flex flex-col gap-4 ${className ?? ""}`} aria-busy={query.loading}>
37
- <header><h3 className="text-base font-semibold">{card.competitor.name || card.competitor.domain}</h3><p className="text-sm text-muted-foreground">{card.summary}</p></header>
37
+ <header><h3 className="text-base font-medium">{card.competitor.name || card.competitor.domain}</h3><p className="text-sm text-muted-foreground">{card.summary}</p></header>
38
38
  <section aria-label="Lead with"><h4 className="text-sm font-medium">Lead with</h4>
39
39
  {card.talk_track.lead_with.length ? <ul className="list-disc pl-5 text-sm">{card.talk_track.lead_with.map((t) => <li key={t}>{t}</li>)}</ul> : <p className="text-sm text-muted-foreground">No proven advantage yet. Add proof to your catalog or show unverified ones.</p>}
40
40
  </section>
@@ -37,11 +37,11 @@ export function RadarOverview({ onSelectCompetitor, className }: RadarOverviewPr
37
37
  <div className={`flex flex-col gap-5 ${className ?? ""}`}>
38
38
  <dl className="grid grid-cols-2 gap-3 sm:grid-cols-5">
39
39
  {stats.map(([label, value]) => (
40
- <div key={label} className="rounded-md border p-3"><dt className="text-xs text-muted-foreground">{label}</dt><dd className="text-xl font-semibold">{value}</dd></div>
40
+ <div key={label} className="rounded-md border p-3"><dt className="text-xs text-muted-foreground">{label}</dt><dd className="text-xl font-medium">{value}</dd></div>
41
41
  ))}
42
42
  </dl>
43
- {s.scan_stale ? <p role="status" className="text-sm text-amber-700">The last scan{s.last_scan_at ? ` (${s.last_scan_at.slice(0, 10)})` : ""} is older than your cadence.</p> : null}
44
- {s.credit_cap_reached ? <p role="status" className="text-sm text-amber-700">The monthly radar credit cap is reached: automatic scans are paused.</p> : null}
43
+ {s.scan_stale ? <p role="status" className="text-sm text-warning-foreground">The last scan{s.last_scan_at ? ` (${s.last_scan_at.slice(0, 10)})` : ""} is older than your cadence.</p> : null}
44
+ {s.credit_cap_reached ? <p role="status" className="text-sm text-warning-foreground">The monthly radar credit cap is reached: automatic scans are paused.</p> : null}
45
45
 
46
46
  <section aria-label="Threat board"><h3 className="mb-2 text-sm font-medium">Threat board</h3>
47
47
  <ul className="flex flex-col gap-1">
@@ -59,7 +59,7 @@ export function RadarOverview({ onSelectCompetitor, className }: RadarOverviewPr
59
59
  </section>
60
60
  <section aria-label="Recent moves"><h3 className="mb-2 text-sm font-medium">Recent moves</h3>
61
61
  {o.moves.length === 0 ? <p className="text-sm text-muted-foreground">No moves yet.</p> : (
62
- <ul className="flex flex-col gap-1">{o.moves.map((m) => <li key={m.id} className="text-sm">{m.competitor_domain ? <span className="font-medium">{m.competitor_domain}: </span> : null}{m.text || m.title}{m.became_gap ? <span className="ml-1 text-xs text-amber-700">became a gap</span> : null}</li>)}</ul>
62
+ <ul className="flex flex-col gap-1">{o.moves.map((m) => <li key={m.id} className="text-sm">{m.competitor_domain ? <span className="font-medium">{m.competitor_domain}: </span> : null}{m.text || m.title}{m.became_gap ? <span className="ml-1 text-xs text-warning-foreground">became a gap</span> : null}</li>)}</ul>
63
63
  )}
64
64
  </section>
65
65
  </div>
@@ -98,8 +98,8 @@ function Overview({ data, onOpen, onTrack }: { data: RadarOverviewBody; onOpen:
98
98
  return (
99
99
  <div className="space-y-4">
100
100
  <MarketingCard />
101
- {data.summary.scan_stale ? <p role="status" className="text-sm text-amber-700">The last scan{data.summary.last_scan_at ? ` (${data.summary.last_scan_at.slice(0, 10)})` : ""} is older than your cadence. Scan from the Competitors tab.</p> : null}
102
- {data.summary.credit_cap_reached ? <p role="status" className="text-sm text-amber-700">The monthly radar credit cap is reached: automatic scans are paused.</p> : null}
101
+ {data.summary.scan_stale ? <p role="status" className="text-sm text-warning-foreground">The last scan{data.summary.last_scan_at ? ` (${data.summary.last_scan_at.slice(0, 10)})` : ""} is older than your cadence. Scan from the Competitors tab.</p> : null}
102
+ {data.summary.credit_cap_reached ? <p role="status" className="text-sm text-warning-foreground">The monthly radar credit cap is reached: automatic scans are paused.</p> : null}
103
103
  <div className="grid gap-4 lg:grid-cols-2">
104
104
  <SectionCard title="Threat board" description="Competitors ranked by threat score. Open one for its battle card.">
105
105
  <ul className="divide-y">
@@ -125,3 +125,9 @@ un-archive.
125
125
  | `resume_sequence_contact_sequences__sequence_id__contacts__contact_id__resume_post` | POST /sequences/{sequence_id}/contacts/{contact_id}/resume | external | SequenceStats |
126
126
 
127
127
  Recipes (`recipes/`): `create-multichannel-sequence.ts`, `launch-and-enroll.ts`, `sequence-report.ts`, `pause-when-unsafe.ts`.
128
+
129
+ ## Cross-block glue
130
+
131
+ - `AddToSequenceButton` (`components/add-to-sequence-button.tsx`): "Add to sequence" for any selection of contacts (live sequences only, then the enroll dialog with its preflight and confirmation). `SequencesScreen` gains optional `onOpenContact`: an enrolled person gets an "Open in contacts" button.
132
+
133
+ - `RecordSequencesTab` (`components/record-sequences-tab.tsx`): a contact's enrollments with state and step (`getContactSequences`, the API's 50 most recent, with a notice when full), `AddToSequenceButton`, and a confirmed Withdraw from one sequence (`withdrawContactsFromSequences`, needs `contacts:write`). `extraDetailTabs` entry for ContactsScreen.
@@ -21,6 +21,7 @@
21
21
  "create_sequence_sequences_post",
22
22
  "delete_sequence_step_sequences__sequence_id__steps__step_id__delete",
23
23
  "duplicate_sequence_sequences__sequence_id__duplicate_post",
24
+ "get_contact_sequences_contacts__contact_id__sequences_get",
24
25
  "get_outreach_connection_integrations_linkedin_outreach_connection_get",
25
26
  "get_safe_to_send_deliverability_safe_to_send_get",
26
27
  "get_sequence_analytics_sequences__sequence_id__analytics_get",
@@ -41,7 +42,8 @@
41
42
  "resume_sequence_contact_sequences__sequence_id__contacts__contact_id__resume_post",
42
43
  "resume_sequence_sequences__sequence_id__resume_post",
43
44
  "run_sequence_sequences__sequence_id__run_post",
44
- "update_sequence_step_sequences__sequence_id__steps__step_id__patch"
45
+ "update_sequence_step_sequences__sequence_id__steps__step_id__patch",
46
+ "withdraw_contacts_from_sequences_contacts_withdraw_from_sequences_post"
45
47
  ],
46
48
  "components": [
47
49
  {
@@ -189,6 +191,30 @@
189
191
  "write",
190
192
  "external"
191
193
  ]
194
+ },
195
+ {
196
+ "name": "AddToSequenceButton",
197
+ "file": "components/add-to-sequence-button.tsx",
198
+ "purpose": "\"Add to sequence\" for any selection of contacts: pick a live sequence, then the enroll dialog (preflight and confirmation) enrolls them. Drops into ContactsScreen bulk and detail slots.",
199
+ "operations": [
200
+ "list_sequences_sequences_get"
201
+ ],
202
+ "tiers": [
203
+ "read"
204
+ ]
205
+ },
206
+ {
207
+ "name": "RecordSequencesTab",
208
+ "file": "components/record-sequences-tab.tsx",
209
+ "purpose": "The sequences one contact is or was enrolled in, with state and step, Add to sequence, and a confirmed Withdraw; an extraDetailTabs entry for ContactsScreen. Withdraw is a write-tier op (the validator derives tiers from the contract), confirmed anyway with the destructive-styled dialog because it stops outreach.",
210
+ "operations": [
211
+ "get_contact_sequences_contacts__contact_id__sequences_get",
212
+ "withdraw_contacts_from_sequences_contacts_withdraw_from_sequences_post"
213
+ ],
214
+ "tiers": [
215
+ "read",
216
+ "write"
217
+ ]
192
218
  }
193
219
  ],
194
220
  "hooks": [],
@@ -243,6 +269,7 @@
243
269
  "scopes": [
244
270
  "campaigns:read",
245
271
  "contacts:read",
272
+ "contacts:write",
246
273
  "integrations:read",
247
274
  "lists:read",
248
275
  "sequences:delete",
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AddToSequenceButton: "Add to sequence" for any selection of contacts. Opens a sheet with every live sequence,
5
+ * then the enroll dialog (which runs the sending preflight and asks the person to confirm before anything is sent).
6
+ * Drop it into ContactsScreen's `renderBulkActions` / `renderDetailActions`.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listSequences GET /sequences?page=&limit=100 (read; pages through the whole list, live ones only)
10
+ * 2. EnrollDialog: getSafeToSend, getSequence, addContactsToSequence (external: confirmed in the dialog)
11
+ */
12
+
13
+ import { useState } from "react";
14
+ import { Button, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Skeleton } from "@graph8/sdk/ui";
15
+ import { ListPlus } from "lucide-react";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { EmptyState } from "@/g8/components/empty-state";
18
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
19
+ import { g8Get } from "@/g8/lib/g8-client";
20
+ import { EnrollDialog } from "./enroll-dialog";
21
+ import { listOf, statusGroup, type SequenceRow } from "../lib/sequencer-utils";
22
+
23
+ /** Every active sequence, paging through the whole list (the API answers one page at a time). */
24
+ async function activeSequences(signal: AbortSignal): Promise<SequenceRow[]> {
25
+ const out: SequenceRow[] = [];
26
+ for (let page = 1; page <= 50; page++) {
27
+ const env = await g8Get("/sequences", { page, limit: 100 }, { signal });
28
+ const rows = listOf<SequenceRow>(env);
29
+ out.push(...rows.filter((s) => statusGroup(s.status) === "live"));
30
+ const more = (env as { pagination?: { has_next?: boolean } }).pagination?.has_next;
31
+ if (!more || rows.length === 0) break;
32
+ }
33
+ return out;
34
+ }
35
+
36
+ export interface AddToSequenceButtonProps {
37
+ contactIds: number[];
38
+ onEnrolled?: () => void;
39
+ variant?: "outline" | "default";
40
+ }
41
+
42
+ /** Pick a sequence, then enroll the contacts in it. The enroll dialog shows the sending preflight before anything is sent. */
43
+ export function AddToSequenceButton({ contactIds, onEnrolled, variant = "outline" }: AddToSequenceButtonProps) {
44
+ const [step, setStep] = useState<"closed" | "pick" | "enroll">("closed");
45
+ const [sequenceId, setSequenceId] = useState("");
46
+ const sequences = useG8Query(activeSequences, [], { enabled: step === "pick" });
47
+ const chosen = (sequences.data ?? []).find((s) => s.id === sequenceId);
48
+ return (
49
+ <>
50
+ <Button size="sm" variant={variant} disabled={contactIds.length === 0} onClick={() => { setSequenceId(""); setStep("pick"); }}>
51
+ <ListPlus className="mr-1 size-4" aria-hidden />
52
+ Add to sequence
53
+ </Button>
54
+ <Sheet open={step === "pick"} onOpenChange={(o) => { if (!o) setStep("closed"); }}>
55
+ <SheetContent className="w-full sm:max-w-md">
56
+ <SheetHeader>
57
+ <SheetTitle>Add to sequence</SheetTitle>
58
+ <SheetDescription>{contactIds.length === 1 ? "1 contact" : `${contactIds.length} contacts`} will be enrolled in the sequence you pick.</SheetDescription>
59
+ </SheetHeader>
60
+ <div className="space-y-3 p-4">
61
+ {sequences.error ? <ApiError error={sequences.error} title="Could not load sequences" onRetry={() => void sequences.refetch()} /> : null}
62
+ {sequences.loading && !sequences.data ? <Skeleton className="h-9 w-full" aria-label="Loading sequences" /> : null}
63
+ {sequences.data && sequences.data.length === 0 ? <EmptyState compact title="No active sequences" description="Launch a sequence on the Sequences page first." /> : null}
64
+ {sequences.data && sequences.data.length > 0 ? (
65
+ <>
66
+ <label htmlFor="enroll-sequence" className="text-sm font-medium">Sequence</label>
67
+ <select id="enroll-sequence" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={sequenceId} onChange={(e) => setSequenceId(e.target.value)}>
68
+ <option value="">Choose a sequence</option>
69
+ {sequences.data.map((s) => <option key={s.id} value={s.id}>{s.name || s.id}</option>)}
70
+ </select>
71
+ <Button disabled={!sequenceId} onClick={() => setStep("enroll")}>Continue</Button>
72
+ </>
73
+ ) : null}
74
+ </div>
75
+ </SheetContent>
76
+ </Sheet>
77
+ {step === "enroll" && chosen ? (
78
+ <EnrollDialog open sequenceId={chosen.id} sequenceName={chosen.name ?? undefined} contactIds={contactIds} onClose={() => setStep("closed")} onEnrolled={() => { setStep("closed"); onEnrolled?.(); }} />
79
+ ) : null}
80
+ </>
81
+ );
82
+ }
@@ -46,7 +46,7 @@ function Verdict() {
46
46
  const v = objectOf(verdict.data);
47
47
  if ((v.safe_to_send ?? v.safe) !== false) return null;
48
48
  return (
49
- <p role="alert" className="mt-2 rounded bg-amber-50 p-2 text-amber-900">
49
+ <p role="alert" className="mt-2 rounded bg-warning p-2 text-warning-foreground">
50
50
  Deliverability warning: {typeof v.reason === "string" ? v.reason : "sending is not recommended right now."}
51
51
  </p>
52
52
  );
@@ -81,7 +81,7 @@ export function EnrollDialog({ open, sequenceId, sequenceName, contactIds, listI
81
81
  <p>They start receiving this sequence's steps. Contacts already enrolled are skipped.</p>
82
82
  {note ? <div className="mt-2">{note}</div> : null}
83
83
  {missingList ? (
84
- <p role="alert" className="mt-2 text-red-700">
84
+ <p role="alert" className="mt-2 text-error-foreground">
85
85
  This sequence has no audience list. Pass the list these contacts came from as `listId`.
86
86
  </p>
87
87
  ) : null}