@graph8/sdk 0.361.0 → 0.370.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,154 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PagesScreen: the finished marketing-site workspace. No props needed.
5
+ *
6
+ * Composes: ScreenLayout (title, tabs, stats), SplitView + DetailPanel (landing page list and the selected
7
+ * page's editor, deep link `?id=`), PageList (list, filter, blank or AI create, publish, unpublish, delete),
8
+ * PageEditor (details, sections, preview, versions, publish), ContentLibrary, BrandKitEditor, SeoReport.
9
+ *
10
+ * Every record is deep-linkable: `?id=` opens a landing page (Edit and Details tabs) or, on the Content tab
11
+ * (`?tab=content`), a content item with all of its fields and actions; the Brand kit tab is the one brand record.
12
+ * `&view=full` shows the open record as a full page.
13
+ *
14
+ * User flows: see every landing page with its state, create one (blank is free, AI is a confirmed spend), open it
15
+ * beside the list, edit its sections, preview, restore a version, publish or unpublish (confirmed), delete
16
+ * (confirmed); manage the content inventory (add by URL, edit, SEO enrichment, delete); re-scan the brand kit and
17
+ * choose page layouts; run an SEO audit and track keywords. The editor changes text fields of sections-mode pages;
18
+ * freeform HTML pages can be previewed, published and versioned, and their HTML is edited in the graph8 app.
19
+ *
20
+ * SDK operations: listLandingPages (for the headline numbers only); everything else lives in the composed parts,
21
+ * which confirm every publish, unpublish, delete, AI generation and paid audit.
22
+ */
23
+
24
+ import { useState } from "react";
25
+ import type { ReactNode } from "react";
26
+ import { Globe } from "lucide-react";
27
+ import { Skeleton } from "@graph8/sdk/ui";
28
+ import { ApiError } from "@/g8/components/api-error";
29
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
30
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
31
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
32
+ import { SplitView } from "@/g8/components/screen/split-view";
33
+ import { useUrlState } from "@/g8/hooks/use-url-state";
34
+ import { g8Get } from "@/g8/lib/g8-client";
35
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
36
+ import { StatusPill } from "@/g8/components/screen/status-pill";
37
+ import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
38
+ import type { LandingPageRow } from "../lib/studio";
39
+ import { BrandKitEditor } from "./brand-kit-editor";
40
+ import { ContentLibrary } from "./content-library";
41
+ import { PageEditor } from "./page-editor";
42
+ import { PageList } from "./page-list";
43
+ import { SeoReport } from "./seo-report";
44
+
45
+ export interface PagesScreenProps {
46
+ /** Prefill the SEO audit domain. */
47
+ defaultDomain?: string;
48
+ renderDetailActions?: (pageId: string) => ReactNode;
49
+ extraDetailTabs?: { id: string; label: string; render: (pageId: string) => ReactNode }[];
50
+ toolbarActions?: ReactNode;
51
+ className?: string;
52
+ }
53
+
54
+ type Tab = "pages" | "content" | "brand" | "seo";
55
+
56
+ /** Every field of one landing page (name and slug are edited in the Edit tab). */
57
+ function PageDetails({ pageId }: { pageId: string }) {
58
+ const res = useG8Query((signal) => g8Get(`/landing-pages/${encodeURIComponent(pageId)}`, undefined, { signal }), [pageId]);
59
+ if (res.error) return <ApiError error={res.error} title="Could not load the page" onRetry={() => void res.refetch()} />;
60
+ if (!res.data) return <Skeleton className="h-32 w-full" aria-busy="true" />;
61
+ const d = asRec(res.data.data);
62
+ const n = (k: string): string => (d[k] == null || d[k] === "" ? "Not set" : text(d[k]));
63
+ const sections = Array.isArray(d.sections) ? d.sections.length : null;
64
+ return (
65
+ <KeyValueList
66
+ columns={2}
67
+ items={[
68
+ { label: "Name", value: n("name") },
69
+ { label: "Status", value: n("status") },
70
+ { label: "Slug", value: n("slug") },
71
+ { label: "Mode", value: d.generation_mode === "freeform" ? "Freeform HTML" : "Sections" },
72
+ { label: "Sections", value: sections == null ? "None (freeform)" : String(sections) },
73
+ { label: "Live URL", value: n("published_url") },
74
+ { label: "Description", value: n("description") },
75
+ { label: "Created", value: shortDate(d.created_at) || "Not set" },
76
+ { label: "Updated", value: shortDate(d.updated_at) || "Not set" },
77
+ { label: "Page id", value: n("id") },
78
+ ...(d.error_message ? [{ label: "Last error", value: text(d.error_message) }] : []),
79
+ ]}
80
+ />
81
+ );
82
+ }
83
+
84
+ export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTabs, toolbarActions, className }: PagesScreenProps) {
85
+ const [tabParam, setTabParam] = useUrlState("tab", "pages");
86
+ const tab = (["pages", "content", "brand", "seo"].includes(tabParam) ? tabParam : "pages") as Tab;
87
+ const [pageId, setPageId] = useUrlState("id", "");
88
+ const [view, setView] = useUrlState("view", "");
89
+ const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
90
+ // The list owns its own fetch; bumping this remounts it after the editor changed something (publish, rename).
91
+ const [rev, setRev] = useState(0);
92
+ const pages = useG8Query((signal) => g8Get("/landing-pages", { limit: 100 }, { signal }), [rev]);
93
+ const data = asRec(pages.data?.data);
94
+ const rows = rowsOf<LandingPageRow>(data, "landing_pages");
95
+ const total = num(data.total) ?? rows.length;
96
+ const published = rows.filter((p) => p.status === "published").length;
97
+ const current = tab === "pages" ? rows.find((p) => p.id === pageId) : undefined;
98
+
99
+ const detail = tab === "pages" && pageId ? (
100
+ <DetailPanel
101
+ title={current?.name || "Landing page"}
102
+ subtitle={current?.published_url ?? undefined}
103
+ badges={current ? <StatusPill tone={current.status === "published" ? "positive" : "neutral"}>{current.status}</StatusPill> : undefined}
104
+ actions={renderDetailActions?.(pageId)}
105
+ onClose={() => { setPageId(null); setView(null); }}
106
+ {...expandProps}
107
+ tabs={[
108
+ { id: "edit", label: "Edit", content: <PageEditor key={pageId} pageId={pageId} onSaved={() => setRev((r) => r + 1)} /> },
109
+ { id: "details", label: "Details", content: <PageDetails key={`${pageId}:${rev}`} pageId={pageId} /> },
110
+ ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(pageId) })),
111
+ ]}
112
+ />
113
+ ) : null;
114
+
115
+ return (
116
+ <ScreenLayout
117
+ className={className}
118
+ title="Pages"
119
+ description="Landing pages, your content library, the brand kit and SEO in one place."
120
+ icon={<Globe className="size-5" />}
121
+ actions={toolbarActions}
122
+ tabs={[
123
+ { id: "pages", label: "Landing pages", count: total },
124
+ { id: "content", label: "Content" },
125
+ { id: "brand", label: "Brand kit" },
126
+ { id: "seo", label: "SEO" },
127
+ ]}
128
+ activeTab={tab}
129
+ onTabChange={(id) => { setTabParam(id === "pages" ? null : id); setPageId(null); setView(null); }}
130
+ stats={tab === "pages" ? [
131
+ { label: "Landing pages", value: total.toLocaleString() },
132
+ { label: "Published", value: published.toLocaleString(), tone: "positive" },
133
+ { label: "Drafts", value: Math.max(0, rows.length - published).toLocaleString() },
134
+ ] : undefined}
135
+ >
136
+ {tab === "pages" ? (
137
+ <>
138
+ {pages.error ? <ApiError error={pages.error} title="Could not load landing pages" onRetry={() => void pages.refetch()} /> : null}
139
+ <SplitView
140
+ detailOpen={Boolean(pageId)}
141
+ onCloseDetail={() => { setPageId(null); setView(null); }}
142
+ expanded={view === "full"}
143
+ onExpandedChange={(e) => setView(e ? "full" : null)}
144
+ detailWidth="xl"
145
+ list={<PageList key={rev} onOpen={(id) => setPageId(id)} onDeleted={(id) => { if (id === pageId) { setPageId(null); setView(null); } setRev((r) => r + 1); }} />}
146
+ detail={detail}
147
+ />
148
+ </>
149
+ ) : tab === "content" ? <ContentLibrary selectedId={pageId || null} onSelectedChange={(id) => setPageId(id)} />
150
+ : tab === "brand" ? <BrandKitEditor />
151
+ : <SeoReport defaultDomain={defaultDomain} />}
152
+ </ScreenLayout>
153
+ );
154
+ }
@@ -11,6 +11,22 @@ Install with `npx @graph8/sdk add quotes`.
11
11
  | `QuoteViewer` | One quote with totals and status-based actions: send/resend, edit as draft, duplicate, delete. |
12
12
  | `SignatureStatus` | Envelope state, signers, activity, reminder. |
13
13
 
14
+ ## Screens
15
+
16
+ `QuotesScreen` (`components/quotes-screen.tsx`): `<QuotesScreen />`, optionally `dealId` or `companyId` to scope it.
17
+
18
+ - Headline numbers by status (drafts, out for signature, accepted, declined) and the quote list with its status filter.
19
+ - Open a quote (deep link `?id=<quote id>`): line items, totals, signer, signature progress, with the actions its status
20
+ allows. Send, resend, signature reminder and edit-as-draft are external and confirmed; delete is destructive and
21
+ confirmed; duplicate and saving a draft email nobody.
22
+ - **New quote** asks for the deal first (search by name, skipped when `dealId` is given because graph8 attaches every
23
+ quote to a deal), then opens the builder (details, signer, line items from the quotable products, live totals).
24
+ - Edit a draft from the viewer in a side sheet.
25
+ - Slots, all optional: `renderDetailActions(quoteId)`, `extraDetailTabs`, `toolbarActions`.
26
+
27
+ The deal search calls `GET /deals`, so the key also needs `deals:read` and the proxy `allowedPrefixes` need `deals`.
28
+
29
+
14
30
  ## Flow, in order (and the ids passed between steps)
15
31
 
16
32
  1. `GET /quotable-products` -> items with `prices[]`. A line item needs `stripe_product_id` + `stripe_price_id`, or `manual_product_id`
@@ -71,6 +87,7 @@ and **delete** (destructive). From code, never call `sendQuote` without the pers
71
87
  | `duplicateQuote` | POST `/api/v1/quotes/{quote_id}/duplicate` | external | `quotes:run` |
72
88
  | `deleteQuote` | DELETE `/api/v1/quotes/{quote_id}` | destructive | `quotes:delete` |
73
89
  | `resendEnvelope` | POST `/api/v1/envelopes/{envelope_id}/resend` | external | `quotes:run` |
90
+ | `listDeals` | GET `/api/v1/deals` | read | `deals:read` (the deal picker in QuotesScreen) |
74
91
 
75
92
  ## Recipes
76
93
 
@@ -14,17 +14,19 @@
14
14
  "deals"
15
15
  ],
16
16
  "operations": [
17
- "list_quotes_quotes_get",
18
- "get_quote_quotes__quote_id__get",
19
- "search_quote_contacts_quote_contacts_get",
20
17
  "create_quote_quotes_post",
21
- "update_quote_quotes__quote_id__patch",
18
+ "delete_quote_quotes__quote_id__delete",
19
+ "duplicate_quote_quotes__quote_id__duplicate_post",
20
+ "edit_quote_as_draft_quotes__quote_id__edit_as_draft_post",
21
+ "get_deal_deals__deal_id__get",
22
+ "get_quote_quotes__quote_id__get",
23
+ "list_deals_deals_get",
22
24
  "list_quotable_products_quotable_products_get",
25
+ "list_quotes_quotes_get",
26
+ "resend_envelope_envelopes__envelope_id__resend_post",
27
+ "search_quote_contacts_quote_contacts_get",
23
28
  "send_quote_quotes__quote_id__send_post",
24
- "edit_quote_as_draft_quotes__quote_id__edit_as_draft_post",
25
- "duplicate_quote_quotes__quote_id__duplicate_post",
26
- "delete_quote_quotes__quote_id__delete",
27
- "resend_envelope_envelopes__envelope_id__resend_post"
29
+ "update_quote_quotes__quote_id__patch"
28
30
  ],
29
31
  "components": [
30
32
  {
@@ -93,6 +95,31 @@
93
95
  "read",
94
96
  "external"
95
97
  ]
98
+ },
99
+ {
100
+ "name": "QuotesScreen",
101
+ "file": "components/quotes-screen.tsx",
102
+ "purpose": "Finished quotes product: status numbers, list, viewer with send and signature tracking, deal-first builder and draft editing.",
103
+ "operations": [
104
+ "list_deals_deals_get",
105
+ "list_quotes_quotes_get"
106
+ ],
107
+ "tiers": [
108
+ "read"
109
+ ]
110
+ },
111
+ {
112
+ "name": "ProductCatalog",
113
+ "file": "components/quote-related.tsx",
114
+ "purpose": "Quotable products with a full product record (fields and prices); also exports QuoteDealTab, the deal a quote belongs to.",
115
+ "operations": [
116
+ "list_quotable_products_quotable_products_get",
117
+ "get_quote_quotes__quote_id__get",
118
+ "get_deal_deals__deal_id__get"
119
+ ],
120
+ "tiers": [
121
+ "read"
122
+ ]
96
123
  }
97
124
  ],
98
125
  "hooks": [
@@ -146,6 +173,7 @@
146
173
  }
147
174
  ],
148
175
  "scopes": [
176
+ "deals:read",
149
177
  "quotes:delete",
150
178
  "quotes:read",
151
179
  "quotes:run"
@@ -178,6 +206,7 @@
178
206
  }
179
207
  ],
180
208
  "peerDependencies": {
181
- "react": ">=18"
209
+ "react": ">=18",
210
+ "lucide-react": ">=0.400.0"
182
211
  }
183
212
  }
@@ -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) => (
@@ -25,17 +25,21 @@ export interface QuoteListProps {
25
25
  onOpen?: (quoteId: string) => void;
26
26
  onNew?: () => void;
27
27
  pageSize?: number;
28
+ /** Change it to reload the current page after a save or delete elsewhere on the screen. */
29
+ refreshKey?: number;
30
+ /** Mark this quote row as the open one. */
31
+ selectedId?: string | null;
28
32
  className?: string;
29
33
  }
30
34
 
31
35
  const STATUSES = ["", "draft", "sent", "viewed", "accepted", "declined", "voided", "expired", "archived"];
32
36
 
33
- export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, className }: QuoteListProps) {
37
+ export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, refreshKey, selectedId, className }: QuoteListProps) {
34
38
  const [status, setStatus] = useState("");
35
39
  const [page, setPage] = useState(1);
36
40
  const q = useG8Query(
37
41
  (signal) => g8Get("/quotes", { status: status || undefined, deal_id: dealId, mashup_company_id: companyId, page, limit: pageSize }, { signal }),
38
- [status, dealId, companyId, page, pageSize],
42
+ [status, dealId, companyId, page, pageSize, refreshKey],
39
43
  );
40
44
  const data = body(q.data);
41
45
  const rows = (Array.isArray(data.items) ? data.items : []) as Rec[];
@@ -57,7 +61,7 @@ export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, cla
57
61
  <TableHeader><TableRow><TableHead>Quote</TableHead><TableHead>Status</TableHead><TableHead className="text-right">Total</TableHead><TableHead>Sent</TableHead></TableRow></TableHeader>
58
62
  <TableBody>
59
63
  {rows.map((r) => (
60
- <TableRow key={text(r.id)}>
64
+ <TableRow key={text(r.id)} data-state={selectedId === text(r.id) ? "selected" : undefined}>
61
65
  <TableCell><button type="button" className="font-medium hover:underline" onClick={() => onOpen?.(text(r.id))}>{text(r.title, "Untitled quote")}</button><div className="text-muted-foreground text-xs">{text(r.quote_number)}</div></TableCell>
62
66
  <TableCell>{statusLabel(r.status)}</TableCell>
63
67
  <TableCell className="text-right">{money(r.total, text(r.currency, "USD"))}</TableCell>
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Catalog and related-record views for the quotes screen.
5
+ *
6
+ * ProductCatalog: every quotable product (Stripe and manual) as a list, with a full product record (all fields and
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.
9
+ *
10
+ * SDK operations, in order:
11
+ * ProductCatalog: GET /quotable-products?page&limit (quotes.listQuotableProducts) read
12
+ * QuoteDealTab: GET /quotes/{id} (quotes.getQuote) read - finds the deal id
13
+ * GET /deals/{id} (deals.getDeal) read
14
+ * Nothing here is billable, external or destructive.
15
+ */
16
+ import { useState } from "react";
17
+ import { Package } from "lucide-react";
18
+ import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
22
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
23
+ import { SectionCard } from "@/g8/components/screen/section-card";
24
+ import { SplitView } from "@/g8/components/screen/split-view";
25
+ import { StatusPill } from "@/g8/components/screen/status-pill";
26
+ import { useUrlState } from "@/g8/hooks/use-url-state";
27
+ import { g8Get } from "@/g8/lib/g8-client";
28
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
29
+ import { formatMoney, type Deal } from "@/g8/deals/lib/deals-types";
30
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
31
+ import { body, frequencyOf, money, text, type QuotableProduct, type ProductPrice } from "../lib/quotes";
32
+ import { useBrand } from "@/g8/hooks/use-brand";
33
+
34
+ const productKey = (p: QuotableProduct) => p.stripe_product_id ?? p.manual_product_id ?? p.name;
35
+ const cadence = (p: ProductPrice) => (frequencyOf(p.recurring_interval) === "one_time" ? "One time" : `Per ${p.recurring_interval}`);
36
+
37
+ export interface ProductCatalogProps {
38
+ /** Show the Start a quote action; wire it to your New quote flow. */
39
+ onStartQuote?: () => void;
40
+ pageSize?: number;
41
+ }
42
+
43
+ export function ProductCatalog({ onStartQuote, pageSize = 50 }: ProductCatalogProps) {
44
+ const brand = useBrand();
45
+ const [selected, setSelected] = useUrlState("product");
46
+ const [mode, setMode] = useUrlState("view");
47
+ const q = useG8Query((signal) => g8Get("/quotable-products", { page: 1, limit: pageSize }, { signal }), [pageSize]);
48
+ const data = body(q.data);
49
+ const items = (Array.isArray(data.items) ? data.items : []) as QuotableProduct[];
50
+ const product = items.find((p) => productKey(p) === selected);
51
+ const close = () => { setSelected(null); setMode(null); };
52
+
53
+ 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 ? (
54
+ <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.`} />
55
+ ) : (
56
+ <Table>
57
+ <TableHeader><TableRow><TableHead>Product</TableHead><TableHead>Source</TableHead><TableHead className="text-right">Prices</TableHead></TableRow></TableHeader>
58
+ <TableBody>
59
+ {items.map((p) => (
60
+ <TableRow key={productKey(p)} className="cursor-pointer" data-state={productKey(p) === selected ? "selected" : undefined} {...rowActivationProps(() => setSelected(productKey(p)))}>
61
+ <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>
62
+ <TableCell><StatusPill tone="neutral">{p.source === "stripe" ? "Stripe" : "Manual"}</StatusPill></TableCell>
63
+ <TableCell className="text-right tabular-nums">{p.prices.length}</TableCell>
64
+ </TableRow>
65
+ ))}
66
+ </TableBody>
67
+ </Table>
68
+ );
69
+
70
+ const detail = product ? (
71
+ <DetailPanel
72
+ title={product.name}
73
+ subtitle={product.category ?? undefined}
74
+ badges={<StatusPill tone="neutral">{product.source === "stripe" ? "Stripe" : "Manual"}</StatusPill>}
75
+ actions={onStartQuote ? <Button size="sm" onClick={onStartQuote}>Start a quote</Button> : undefined}
76
+ onClose={close}
77
+ onExpand={mode === "full" ? undefined : () => setMode("full")}
78
+ onCollapse={mode === "full" ? () => setMode(null) : undefined}
79
+ tabs={[{ id: "product", label: "Product", content: (
80
+ <div className="space-y-4">
81
+ <SectionCard title="Details">
82
+ <KeyValueList columns={2} items={[
83
+ { label: "Name", value: product.name }, { label: "Source", value: product.source }, { label: "Category", value: product.category },
84
+ { label: "Description", value: product.description }, { label: "Stripe product id", value: product.stripe_product_id, mono: true }, { label: "Manual product id", value: product.manual_product_id, mono: true },
85
+ ]} />
86
+ </SectionCard>
87
+ <SectionCard title="Prices" description="A quote line copies one of these prices.">
88
+ {product.prices.length === 0 ? <p className="text-sm text-muted-foreground">No flat price. Tiered Stripe prices cannot be quoted.</p> : (
89
+ <Table>
90
+ <TableHeader><TableRow><TableHead>Billing</TableHead><TableHead>Type</TableHead><TableHead className="text-right">Amount</TableHead></TableRow></TableHeader>
91
+ <TableBody>{product.prices.map((p, i) => <TableRow key={p.stripe_price_id ?? i}><TableCell>{cadence(p)}</TableCell><TableCell>{text(p.type)}</TableCell><TableCell className="text-right tabular-nums">{money(p.unit_amount, (p.currency ?? "usd").toUpperCase())}</TableCell></TableRow>)}</TableBody>
92
+ </Table>
93
+ )}
94
+ </SectionCard>
95
+ </div>
96
+ ) }]}
97
+ />
98
+ ) : null;
99
+
100
+ return <SplitView detailOpen={Boolean(product)} onCloseDetail={close} expanded={mode === "full" && Boolean(product)} onExpandedChange={(f) => setMode(f ? "full" : null)} list={list} detail={detail} />;
101
+ }
102
+
103
+ export function QuoteDealTab({ quoteId, onOpenDeal }: { quoteId: string; onOpenDeal?: (dealId: string) => void }) {
104
+ const q = useG8Query(async (signal) => {
105
+ const quote = body(await g8Get(`/quotes/${encodeURIComponent(quoteId)}`, undefined, { signal }));
106
+ const dealId = text(quote.deal_id);
107
+ if (!dealId) return null;
108
+ const env = await g8Get<Deal>(`/deals/${encodeURIComponent(dealId)}`, undefined, { signal });
109
+ return (env.data ?? env) as unknown as Deal;
110
+ }, [quoteId]);
111
+ if (q.error) return <ApiError error={q.error} title="Could not load the deal" onRetry={() => void q.refetch()} />;
112
+ if (q.loading && q.data === undefined) return <Skeleton className="h-24 w-full" aria-label="Loading deal" />;
113
+ const d = q.data;
114
+ if (!d) return <EmptyState title="No deal on this quote" />;
115
+ return (
116
+ <SectionCard title={d.name || "Deal"} actions={onOpenDeal ? <Button size="sm" variant="outline" onClick={() => onOpenDeal(d.id)}>Open deal</Button> : undefined}>
117
+ <KeyValueList columns={2} items={[
118
+ { 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
+ { label: "Close date", value: d.close_date?.slice(0, 10) }, { label: "Pipeline", value: d.pipeline_name }, { label: "Contacts", value: d.contact_count },
120
+ ]} />
121
+ </SectionCard>
122
+ );
123
+ }
@@ -0,0 +1,169 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuotesScreen: the finished quotes product. Every quote with its status, a viewer for the open one, a builder for
5
+ * new and draft quotes, and e-signature tracking. No props are required.
6
+ *
7
+ * It composes: ProductCatalog (Products tab), QuoteDealTab, QuoteList (status filter, paging), QuoteViewer (totals, send, resend, edit as draft, duplicate,
8
+ * delete, e-signature status and reminder), QuoteBuilder (details, signer, line items from ProductPicker) and a deal
9
+ * picker, because graph8 attaches every quote to a deal.
10
+ *
11
+ * Flows:
12
+ * - Headline numbers by status: drafts, out for signature, accepted, declined.
13
+ * - Open a quote (deep link ?id=<quote id>, full page with &view=full) -> line items, totals, signer, payment
14
+ * terms, signature progress, and a Deal tab. The Products tab lists every quotable product; open one for all
15
+ * its fields and prices (?tab=products&product=<id>).
16
+ * - New quote: choose the deal (search by name; skipped when `dealId` is passed), then the builder. Saving a draft
17
+ * emails nobody, so it needs no confirmation.
18
+ * - Edit a draft from the viewer; duplicate; delete a draft (confirmed).
19
+ * - Send for signature, resend, send a signature reminder, and edit a sent quote as a new draft: all external
20
+ * actions, each behind ConfirmSpendDialog because they reach a real person.
21
+ *
22
+ * SDK operations (through the composed parts): listQuotes, getQuote, createQuote, updateQuotePatch,
23
+ * searchQuoteContacts, listQuotableProducts, sendQuote (external), editQuoteAsDraft (external), duplicateQuote,
24
+ * deleteQuote (destructive), resendEnvelope (external) and the deals block's listDeals (the deal picker).
25
+ *
26
+ * Slots (all optional): renderDetailActions, extraDetailTabs, toolbarActions.
27
+ */
28
+ import { useState, type ReactNode } from "react";
29
+ import { FileText, Plus, Search } from "lucide-react";
30
+ import { Button, Input } from "@graph8/sdk/ui";
31
+ import { ApiError } from "@/g8/components/api-error";
32
+ import { EmptyState } from "@/g8/components/empty-state";
33
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
34
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
35
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
36
+ import { SplitView } from "@/g8/components/screen/split-view";
37
+ import { useUrlState } from "@/g8/hooks/use-url-state";
38
+ import { g8Get } from "@/g8/lib/g8-client";
39
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
40
+ import { formatMoney, type Deal } from "@/g8/deals/lib/deals-types";
41
+ import { useDebounced } from "../hooks/use-debounced";
42
+ import { body } from "../lib/quotes";
43
+ import { QuoteBuilder } from "./quote-builder";
44
+ import { QuoteList } from "./quote-list";
45
+ import { ProductCatalog, QuoteDealTab } from "./quote-related";
46
+ import { QuoteViewer } from "./quote-viewer";
47
+
48
+ export interface QuotesScreenProps {
49
+ /** Show only the quotes of this deal and create new ones for it. */
50
+ dealId?: string;
51
+ /** Show only the quotes of this company (mashup company id). */
52
+ companyId?: number;
53
+ renderDetailActions?: (quoteId: string) => ReactNode;
54
+ extraDetailTabs?: Array<{ id: string; label: string; render: (quoteId: string) => ReactNode }>;
55
+ toolbarActions?: ReactNode;
56
+ /** Open the quote's deal in your deals screen; without it the Deal tab is read-only. */
57
+ onOpenDeal?: (dealId: string) => void;
58
+ }
59
+
60
+ const STATUS_STATS: Array<{ status: string; label: string; tone?: "positive" | "negative" | "info" }> = [
61
+ { status: "draft", label: "Drafts" },
62
+ { status: "sent", label: "Out for signature", tone: "info" },
63
+ { status: "accepted", label: "Accepted", tone: "positive" },
64
+ { status: "declined", label: "Declined", tone: "negative" },
65
+ ];
66
+
67
+ function useStatusTotals(dealId: string | undefined, companyId: number | undefined, refreshKey: number) {
68
+ return useG8Query(async (signal) => {
69
+ const totals = await Promise.all(STATUS_STATS.map(async (s) => {
70
+ const data = body(await g8Get("/quotes", { status: s.status, deal_id: dealId, mashup_company_id: companyId, page: 1, limit: 1 }, { signal }));
71
+ return typeof data.total === "number" ? data.total : 0;
72
+ }));
73
+ return totals;
74
+ }, [dealId, companyId, refreshKey]);
75
+ }
76
+
77
+ /** Step one of a new quote when no deal is given: find the deal the quote belongs to. */
78
+ function DealStep({ onPick }: { onPick: (deal: Deal) => void }) {
79
+ const [text, setText] = useState("");
80
+ const term = useDebounced(text);
81
+ const deals = useG8Query(async (signal) => {
82
+ const env = await g8Get<Deal[]>("/deals", { search: term || undefined, limit: 10, page: 1 }, { signal });
83
+ return env.data ?? [];
84
+ }, [term]);
85
+ return (
86
+ <div className="space-y-3">
87
+ <p className="text-sm text-muted-foreground">A quote belongs to a deal. Pick the deal first.</p>
88
+ <div className="relative">
89
+ <Search className="pointer-events-none absolute left-2 top-2.5 size-4 text-muted-foreground" aria-hidden />
90
+ <Input aria-label="Search deals" className="pl-8" placeholder="Search deals by name" value={text} onChange={(e) => setText(e.target.value)} />
91
+ </div>
92
+ {deals.error ? <ApiError error={deals.error} title="Could not load deals" onRetry={() => void deals.refetch()} /> : null}
93
+ {!deals.loading && !deals.error && (deals.data ?? []).length === 0 ? <EmptyState title={term ? "No deals match" : "No deals yet"} description="Create a deal first, then quote it." /> : null}
94
+ <ul className="divide-y rounded-md border empty:hidden" aria-label="Deals">
95
+ {(deals.data ?? []).map((d) => (
96
+ <li key={d.id}>
97
+ <button type="button" className="flex w-full items-center justify-between gap-2 p-3 text-left text-sm hover:bg-muted" onClick={() => onPick(d)}>
98
+ <span><span className="font-medium">{d.name || "Untitled deal"}</span><span className="block text-xs text-muted-foreground">{d.stage_name ?? ""}</span></span>
99
+ <span className="tabular-nums text-muted-foreground">{formatMoney(d.amount, d.currency)}</span>
100
+ </button>
101
+ </li>
102
+ ))}
103
+ </ul>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ export function QuotesScreen({ dealId, companyId, renderDetailActions, extraDetailTabs, toolbarActions, onOpenDeal }: QuotesScreenProps) {
109
+ const [selectedId, setSelectedId] = useUrlState("id");
110
+ const [mode, setMode] = useUrlState("view");
111
+ const [tabParam, setTab] = useUrlState("tab", "quotes");
112
+ const tab = tabParam === "products" ? "products" : "quotes";
113
+ const [version, setVersion] = useState(0);
114
+ const [creating, setCreating] = useState(false);
115
+ const [pickedDeal, setPickedDeal] = useState<Deal | null>(null);
116
+ const [editingId, setEditingId] = useState<string | null>(null);
117
+ const totals = useStatusTotals(dealId, companyId, version);
118
+ const refresh = () => setVersion((v) => v + 1);
119
+ const close = () => { setSelectedId(null); setMode(null); };
120
+ const openNew = () => { setPickedDeal(null); setCreating(true); };
121
+ const saved = (id: string) => { setCreating(false); setEditingId(null); refresh(); setSelectedId(id); };
122
+ const builderDeal = dealId ?? pickedDeal?.id;
123
+
124
+ const stats = STATUS_STATS.map((s, i) => ({ label: s.label, value: totals.data ? (totals.data[i] ?? 0).toLocaleString() : "-", tone: s.tone }));
125
+
126
+ const detail = selectedId ? (
127
+ <DetailPanel
128
+ title="Quote"
129
+ actions={renderDetailActions?.(selectedId)}
130
+ onClose={close}
131
+ onExpand={mode === "full" ? undefined : () => setMode("full")}
132
+ onCollapse={mode === "full" ? () => setMode(null) : undefined}
133
+ tabs={[
134
+ { id: "quote", label: "Quote", content: <QuoteViewer key={selectedId} quoteId={selectedId} onEdit={setEditingId} onOpenQuote={(id) => { refresh(); setSelectedId(id); }} onDeleted={() => { close(); refresh(); }} /> },
135
+ { id: "deal", label: "Deal", content: <QuoteDealTab key={selectedId} quoteId={selectedId} onOpenDeal={onOpenDeal} /> },
136
+ ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selectedId) })),
137
+ ]}
138
+ />
139
+ ) : null;
140
+
141
+ return (
142
+ <ScreenLayout
143
+ title="Quotes"
144
+ description="Build quotes from your products, send them for signature and track who has signed."
145
+ icon={<FileText className="size-5" />}
146
+ actions={<>{toolbarActions}<Button onClick={openNew}><Plus className="size-4" aria-hidden />New quote</Button></>}
147
+ stats={stats}
148
+ tabs={[{ id: "quotes", label: "Quotes" }, { id: "products", label: "Products" }]}
149
+ activeTab={tab}
150
+ onTabChange={(id) => { setTab(id); close(); }}
151
+ >
152
+ {totals.error ? <ApiError error={totals.error} title="Could not load quote numbers" onRetry={() => void totals.refetch()} /> : null}
153
+ {tab === "products" ? <ProductCatalog onStartQuote={openNew} /> : <SplitView
154
+ detailOpen={Boolean(selectedId)}
155
+ onCloseDetail={close}
156
+ expanded={mode === "full" && Boolean(selectedId)}
157
+ onExpandedChange={(full) => setMode(full ? "full" : null)}
158
+ detail={detail}
159
+ list={<QuoteList dealId={dealId} companyId={companyId} refreshKey={version} selectedId={selectedId} onOpen={setSelectedId} onNew={openNew} />}
160
+ />}
161
+ <FormSheet open={creating} onOpenChange={setCreating} title="New quote" description={pickedDeal ? `For ${pickedDeal.name ?? "this deal"}` : undefined}>
162
+ {creating ? (builderDeal ? <QuoteBuilder key={builderDeal} dealId={builderDeal} companyId={companyId} onSaved={saved} /> : <DealStep onPick={setPickedDeal} />) : null}
163
+ </FormSheet>
164
+ <FormSheet open={editingId != null} onOpenChange={(o) => { if (!o) setEditingId(null); }} title="Edit draft quote">
165
+ {editingId ? <QuoteBuilder key={editingId} quoteId={editingId} onSaved={saved} /> : null}
166
+ </FormSheet>
167
+ </ScreenLayout>
168
+ );
169
+ }
@@ -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 |