@graph8/sdk 0.362.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 (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -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
+ }
@@ -22,6 +22,7 @@ import { ApiError } from "@/g8/components/api-error";
22
22
  import { EmptyState } from "@/g8/components/empty-state";
23
23
  import { body, frequencyOf, lineFromProduct, money, text } from "../lib/quotes";
24
24
  import type { LineItem, ProductPrice, QuotableProduct } from "../lib/quotes";
25
+ import { useBrand } from "@/g8/hooks/use-brand";
25
26
 
26
27
  export interface ProductPickerProps {
27
28
  /** Only offer prices in this currency (the quote's currency). */
@@ -34,6 +35,7 @@ export interface ProductPickerProps {
34
35
  const intervalLabel = (p: ProductPrice) => (frequencyOf(p.recurring_interval) === "one_time" ? "one time" : `per ${p.recurring_interval}`);
35
36
 
36
37
  export function ProductPicker({ currency, onPick, pageSize = 25, className }: ProductPickerProps) {
38
+ const brand = useBrand();
37
39
  const [page, setPage] = useState(1);
38
40
  const [search, setSearch] = useState("");
39
41
  const q = useG8Query((signal) => g8Get("/quotable-products", { currency: currency?.toUpperCase(), page, limit: pageSize }, { signal }), [currency, page, pageSize]);
@@ -49,7 +51,7 @@ export function ProductPicker({ currency, onPick, pageSize = 25, className }: Pr
49
51
  {q.error ? <ApiError error={q.error} onRetry={() => void q.refetch()} /> : null}
50
52
  {q.loading && !q.data ? <div className="space-y-2" aria-busy="true">{[0, 1, 2].map((i) => <Skeleton key={i} className="h-12 w-full" />)}</div> : null}
51
53
  {!q.loading && !q.error && shown.length === 0 ? (
52
- <EmptyState title="No quotable products" description="Mark a Stripe product as quotable, or add a manual product, in graph8's quote settings." />
54
+ <EmptyState title="No quotable products" description={`Mark a Stripe product as quotable, or add a manual product, in ${brand.linkLabel}'s quote settings.`} />
53
55
  ) : null}
54
56
  <ul className="divide-y rounded-md border">
55
57
  {shown.map((p) => (
@@ -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
@@ -27,7 +27,10 @@ import { useUrlState } from "@/g8/hooks/use-url-state";
27
27
  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
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
31
+ import { CreateQuoteFromDeal } from "./create-quote-from-deal";
30
32
  import { body, frequencyOf, money, text, type QuotableProduct, type ProductPrice } from "../lib/quotes";
33
+ import { useBrand } from "@/g8/hooks/use-brand";
31
34
 
32
35
  const productKey = (p: QuotableProduct) => p.stripe_product_id ?? p.manual_product_id ?? p.name;
33
36
  const cadence = (p: ProductPrice) => (frequencyOf(p.recurring_interval) === "one_time" ? "One time" : `Per ${p.recurring_interval}`);
@@ -39,6 +42,7 @@ export interface ProductCatalogProps {
39
42
  }
40
43
 
41
44
  export function ProductCatalog({ onStartQuote, pageSize = 50 }: ProductCatalogProps) {
45
+ const brand = useBrand();
42
46
  const [selected, setSelected] = useUrlState("product");
43
47
  const [mode, setMode] = useUrlState("view");
44
48
  const q = useG8Query((signal) => g8Get("/quotable-products", { page: 1, limit: pageSize }, { signal }), [pageSize]);
@@ -48,13 +52,13 @@ export function ProductCatalog({ onStartQuote, pageSize = 50 }: ProductCatalogPr
48
52
  const close = () => { setSelected(null); setMode(null); };
49
53
 
50
54
  const list = q.error ? <ApiError error={q.error} title="Could not load products" onRetry={() => void q.refetch()} /> : q.loading && !q.data ? <Skeleton className="h-40 w-full" aria-label="Loading products" /> : items.length === 0 ? (
51
- <EmptyState icon={<Package className="size-8" />} title="No quotable products" description="Mark a Stripe product as quotable, or add a manual product, in graph8's quote settings." />
55
+ <EmptyState icon={<Package className="size-8" />} title="No quotable products" description={`Mark a Stripe product as quotable, or add a manual product, in ${brand.linkLabel}'s quote settings.`} />
52
56
  ) : (
53
57
  <Table>
54
58
  <TableHeader><TableRow><TableHead>Product</TableHead><TableHead>Source</TableHead><TableHead className="text-right">Prices</TableHead></TableRow></TableHeader>
55
59
  <TableBody>
56
60
  {items.map((p) => (
57
- <TableRow key={productKey(p)} className="cursor-pointer" data-state={productKey(p) === selected ? "selected" : undefined} onClick={() => setSelected(productKey(p))}>
61
+ <TableRow key={productKey(p)} className="cursor-pointer" data-state={productKey(p) === selected ? "selected" : undefined} {...rowActivationProps(() => setSelected(productKey(p)))}>
58
62
  <TableCell><button type="button" className="font-medium hover:underline" onClick={(e) => { e.stopPropagation(); setSelected(productKey(p)); }}>{p.name}</button><div className="text-xs text-muted-foreground">{p.category}</div></TableCell>
59
63
  <TableCell><StatusPill tone="neutral">{p.source === "stripe" ? "Stripe" : "Manual"}</StatusPill></TableCell>
60
64
  <TableCell className="text-right tabular-nums">{p.prices.length}</TableCell>
@@ -110,7 +114,7 @@ export function QuoteDealTab({ quoteId, onOpenDeal }: { quoteId: string; onOpenD
110
114
  const d = q.data;
111
115
  if (!d) return <EmptyState title="No deal on this quote" />;
112
116
  return (
113
- <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" /></>}>
114
118
  <KeyValueList columns={2} items={[
115
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 },
116
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
+ }
@@ -5,6 +5,16 @@ deep-dive reports. Depends only on `foundation`.
5
5
 
6
6
  Install: `npx @graph8/sdk add radar`.
7
7
 
8
+ ## Screens
9
+
10
+ `RadarScreen` (`components/radar-screen.tsx`) is the finished page: copy it as your route. No props are required.
11
+
12
+ - **Overview tab**: stat strip, threat board (click a row to open its battle card), alerts and recent moves.
13
+ - **Competitors tab**: add, bulk-add detected, scan, pause, resume and remove competitors; the selected one opens its battle card
14
+ in a side panel. Deep link: `?id=<competitor id>`. **Reports tab**: generate and open deep-dive reports.
15
+ - Scans and reports spend credits and are confirmed; removing a competitor is confirmed as destructive.
16
+ - Slots: `toolbarActions`, `renderDetailActions(record)`, `extraDetailTabs` (each `{ id, label, render(record) }`), where `record` is `{ kind: "competitor" | "report", id }`.
17
+
8
18
  ## What you get
9
19
 
10
20
  | Component | Use it for | Props |
@@ -87,6 +87,45 @@
87
87
  "read",
88
88
  "billable"
89
89
  ]
90
+ },
91
+ {
92
+ "name": "RadarScreen",
93
+ "file": "components/radar-screen.tsx",
94
+ "purpose": "Finished competitive-radar page: overview with threat board, competitor roster with battle-card panel, and reports.",
95
+ "operations": [
96
+ "get_marketing_overview_analytics_marketing_overview_get",
97
+ "overview_radar_overview_get"
98
+ ],
99
+ "tiers": [
100
+ "read"
101
+ ]
102
+ },
103
+ {
104
+ "name": "CompetitorDetail",
105
+ "file": "components/competitor-detail.tsx",
106
+ "purpose": "Complete screen for one competitor: every field, battle card, scan, pause or resume, remove.",
107
+ "operations": [
108
+ "scan_competitor_radar_competitors__competitor_id__scan_post",
109
+ "update_competitor_radar_competitors__competitor_id__patch",
110
+ "delete_competitor_radar_competitors__competitor_id__delete"
111
+ ],
112
+ "tiers": [
113
+ "billable",
114
+ "destructive"
115
+ ]
116
+ },
117
+ {
118
+ "name": "ReportDetail",
119
+ "file": "components/report-detail.tsx",
120
+ "purpose": "Complete screen for one radar report: status, preview, HTML and PDF links, regenerate.",
121
+ "operations": [
122
+ "list_radar_reports_radar_reports_get",
123
+ "create_radar_report_radar_reports_post"
124
+ ],
125
+ "tiers": [
126
+ "read",
127
+ "billable"
128
+ ]
90
129
  }
91
130
  ],
92
131
  "hooks": [
@@ -184,6 +223,7 @@
184
223
  }
185
224
  ],
186
225
  "peerDependencies": {
187
- "react": ">=18"
226
+ "react": ">=18",
227
+ "lucide-react": ">=0.400"
188
228
  }
189
229
  }
@@ -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>