@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,150 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CreativeDetail: the complete screen for one AI ad creative. Finds it by id (so `?id=` deep links work), shows the image and every
5
+ * copy field, and offers every action: edit headline, primary text, description and call to action; make a new image (billable);
6
+ * move it to the gallery; delete. A creative that is still generating re-reads itself until it finishes.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listAdCreatives GET /ad-creatives?limit=100 (read)
10
+ * 2. updateAdCreative PATCH /ad-creatives/{id} (write)
11
+ * 3. regenerateAdCreativeImage POST /ad-creatives/{id}/regenerate-image (billable, confirmed; no advance price)
12
+ * 4. moveAdCreativeToGallery POST /ad-creatives/{id}/move-to-gallery (write)
13
+ * 5. deleteAdCreative DELETE /ad-creatives/{id} (destructive, confirmed; 404 counts as done)
14
+ * 6. getAdCreativeStatus GET /ad-creatives/{id}/status (read; polled every 4 s while the creative is generating.
15
+ * The payload is open-shaped in the contract: only a string `status` is read, and a change reloads the creative.)
16
+ */
17
+ import { useEffect, useState } from "react";
18
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
24
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
25
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
26
+ import { SectionCard } from "@/g8/components/screen/section-card";
27
+ import { StatusPill } from "@/g8/components/screen/status-pill";
28
+ import type { AdCreative } from "../lib/social-ads-types";
29
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
30
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
31
+
32
+ export interface CreativeDetailProps {
33
+ creativeId: string;
34
+ onClose?: () => void;
35
+ onExpand?: () => void;
36
+ onCollapse?: () => void;
37
+ onChanged?: () => void;
38
+ }
39
+
40
+ const tone = (s: string) => (s === "gallery" || s === "completed" ? "positive" : s === "failed" ? "negative" : s === "generating" ? "info" : "neutral") as "positive" | "negative" | "info" | "neutral";
41
+
42
+ export function CreativeDetail({ creativeId, onClose, onExpand, onCollapse, onChanged }: CreativeDetailProps) {
43
+ const { ask, dialog, failure } = useConfirmAction();
44
+ // Read the one creative by id (GET /ad-creatives/{id}), so an older creative outside the newest page still opens.
45
+ // A 404 means it no longer exists.
46
+ const list = useG8Query(async (signal) => {
47
+ try {
48
+ const body = (await g8Get<AdCreative | { data?: AdCreative }>(`/ad-creatives/${encodeURIComponent(creativeId)}`, undefined, { signal })).data;
49
+ const one = body && typeof body === "object" && "data" in body && body.data ? body.data : (body as AdCreative | null);
50
+ return one && one.id != null ? one : null;
51
+ } catch (err) {
52
+ if ((err as { status?: number })?.status === 404) return null;
53
+ throw err;
54
+ }
55
+ }, [creativeId]);
56
+ const creative = list.data ?? undefined;
57
+ const [form, setForm] = useState({ headline: "", primary_text: "", description: "", cta: "" });
58
+ const generating = creative?.status === "generating";
59
+
60
+ useEffect(() => {
61
+ if (!creative) return;
62
+ setForm({ headline: creative.headline ?? "", primary_text: creative.primary_text ?? "", description: creative.description ?? "", cta: creative.cta ?? "" });
63
+ }, [creative?.id, creative?.headline, creative?.primary_text, creative?.description, creative?.cta]); // eslint-disable-line react-hooks/exhaustive-deps
64
+
65
+ useEffect(() => {
66
+ if (!generating) return;
67
+ const timer = setInterval(() => {
68
+ void g8Get(`/ad-creatives/${creativeId}/status`)
69
+ .then((res) => {
70
+ const next = (res.data as { status?: unknown } | null | undefined)?.status;
71
+ if (typeof next === "string" && next !== "generating") void list.refetch();
72
+ })
73
+ .catch(() => void list.refetch());
74
+ }, 4000);
75
+ return () => clearInterval(timer);
76
+ }, [generating, creativeId, list.refetch]); // eslint-disable-line react-hooks/exhaustive-deps
77
+
78
+ const save = useG8Mutation(() => g8Patch(`/ad-creatives/${creativeId}`, form), { onSuccess: () => { void list.refetch(); onChanged?.(); } });
79
+ const gallery = useG8Mutation(() => g8Post(`/ad-creatives/${creativeId}/move-to-gallery`, {}), { onSuccess: () => { void list.refetch(); onChanged?.(); } });
80
+ const changed = () => { void list.refetch(); onChanged?.(); };
81
+
82
+ if (list.error) return <ApiError error={list.error} title="Could not load the creative" onRetry={() => void list.refetch()} />;
83
+ if (list.data === undefined) return <Skeleton className="h-40 w-full" />;
84
+ if (!creative) return <EmptyState title="Creative not found" description="It may have been deleted." />;
85
+
86
+ const actions = (
87
+ <div className="flex flex-wrap gap-1">
88
+ <AskButton ask={ask} label="New image" tier="billable" title={`Make a new image for "${creative.name}"`} confirmLabel="Generate" disabled={generating}
89
+ description="Uses credits. The API gives no price in advance and your balance is not checked here." run={() => g8Post(`/ad-creatives/${creativeId}/regenerate-image`, {})} onDone={changed} />
90
+ {creative.status !== "gallery" && !generating ? (
91
+ <Button size="sm" variant="outline" disabled={gallery.loading} onClick={() => void gallery.mutate(undefined)}>Move to gallery</Button>
92
+ ) : null}
93
+ <AskButton ask={ask} label="Delete" variant="ghost" tier="destructive" title={`Delete "${creative.name}"`} confirmLabel="Delete"
94
+ description="The creative and its revisions are removed." tolerateStatus={[404]} run={() => g8Delete(`/ad-creatives/${creativeId}`)} onDone={() => { changed(); onClose?.(); }} />
95
+ </div>
96
+ );
97
+
98
+ const field = (key: keyof typeof form, label: string) => (
99
+ <div className="flex flex-col gap-1" key={key}>
100
+ <Label htmlFor={`cd-${key}`}>{label}</Label>
101
+ <Input id={`cd-${key}`} value={form[key]} onChange={(e) => setForm({ ...form, [key]: e.target.value })} />
102
+ </div>
103
+ );
104
+ const tabs = [
105
+ {
106
+ id: "creative",
107
+ label: "Creative",
108
+ content: (
109
+ <div className="space-y-4">
110
+ {creative.image_url ? <img src={creative.image_url} alt={creative.headline ?? creative.name} className="aspect-video w-full rounded object-cover" /> : (
111
+ <div className="flex aspect-video items-center justify-center rounded bg-muted text-sm text-muted-foreground">{generating ? "Generating..." : "No image"}</div>
112
+ )}
113
+ {creative.status === "failed" && creative.error_message ? <p role="alert" className="text-sm text-error-foreground">{creative.error_message}</p> : null}
114
+ <KeyValueList columns={2} items={[
115
+ { label: "Platform", value: creative.platform },
116
+ { label: "Format", value: creative.ad_format },
117
+ { label: "Headline", value: creative.headline ?? "-" },
118
+ { label: "Call to action", value: creative.cta ?? "-" },
119
+ { label: "Primary text", value: creative.primary_text ?? "-" },
120
+ { label: "Description", value: creative.description ?? "-" },
121
+ ]} />
122
+ </div>
123
+ ),
124
+ },
125
+ {
126
+ id: "edit",
127
+ label: "Edit copy",
128
+ content: (
129
+ <SectionCard title="Edit copy">
130
+ <div className="space-y-3">
131
+ {field("headline", "Headline")}
132
+ {field("primary_text", "Primary text")}
133
+ {field("description", "Description")}
134
+ {field("cta", "Call to action")}
135
+ <Button size="sm" disabled={save.loading || generating} onClick={() => void save.mutate(undefined)}>Save copy</Button>
136
+ {save.error ? <ApiError error={save.error} title="Could not save" /> : null}
137
+ {gallery.error ? <ApiError error={gallery.error} title="Could not move to the gallery" /> : null}
138
+ </div>
139
+ </SectionCard>
140
+ ),
141
+ },
142
+ ];
143
+ return (
144
+ <>
145
+ <DetailPanel title={creative.headline || creative.name} subtitle={`${creative.platform} · ${creative.ad_format}`} badges={<StatusPill tone={tone(creative.status)}>{creative.status}</StatusPill>} actions={actions} tabs={tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
146
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
147
+ <ConfirmSpendDialog {...dialog} />
148
+ </>
149
+ );
150
+ }
@@ -32,17 +32,19 @@ import type { AdCreative } from "../lib/social-ads-types";
32
32
  export interface CreativeGalleryProps {
33
33
  /** Show only creatives in this state. Default: all working creatives. */
34
34
  status?: "draft" | "completed" | "gallery";
35
+ /** Called when a person opens a creative (show a CreativeDetail). */
36
+ onSelectCreative?: (creative: AdCreative) => void;
35
37
  className?: string;
36
38
  }
37
39
 
38
40
  interface Specs { platforms: string[]; specs: Record<string, Record<string, { label: string }>> }
39
41
  type Pending = { kind: "create" } | { kind: "regenerate" | "delete"; creative: AdCreative };
40
42
 
41
- export function CreativeGallery({ status, className }: CreativeGalleryProps) {
43
+ export function CreativeGallery({ status, onSelectCreative, className }: CreativeGalleryProps) {
42
44
  const [platform, setPlatform] = useState("");
43
45
  const [form, setForm] = useState({ platform: "meta", format: "", prompt: "", name: "" });
44
46
  const [pending, setPending] = useState<Pending | null>(null);
45
- const [editing, setEditing] = useState<{ id: string; headline: string; primary_text: string } | null>(null);
47
+ const [editing, setEditing] = useState<{ id: string; headline: string; primary_text: string; orig: { headline: string; primary_text: string } } | null>(null);
46
48
  const [busy, setBusy] = useState<string | null>(null);
47
49
  const [failure, setFailure] = useState<unknown>(null);
48
50
 
@@ -68,7 +70,15 @@ export function CreativeGallery({ status, className }: CreativeGalleryProps) {
68
70
  if (pending.kind === "regenerate") return void run(id, () => g8Post(`/ad-creatives/${id}/regenerate-image`, {}));
69
71
  return void run(id, () => g8Delete(`/ad-creatives/${id}`), true);
70
72
  };
71
- const saveCopy = () => editing && void run(editing.id, async () => { await g8Patch(`/ad-creatives/${editing.id}`, { headline: editing.headline, primary_text: editing.primary_text }); setEditing(null); });
73
+ // Only the field(s) the person changed are sent (the backend overwrites whatever it receives, and a regenerate may finish while the copy is open); nothing changed means no request.
74
+ const saveCopy = () => {
75
+ if (!editing) return;
76
+ const changed: Record<string, string> = {};
77
+ if (editing.headline !== editing.orig.headline) changed.headline = editing.headline;
78
+ if (editing.primary_text !== editing.orig.primary_text) changed.primary_text = editing.primary_text;
79
+ if (Object.keys(changed).length === 0) return setEditing(null);
80
+ void run(editing.id, async () => { await g8Patch(`/ad-creatives/${editing.id}`, changed); setEditing(null); });
81
+ };
72
82
 
73
83
  const tier: ConfirmTier = pending?.kind === "delete" ? "destructive" : "billable";
74
84
  const name = pending && pending.kind !== "create" ? pending.creative.name : "";
@@ -101,10 +111,10 @@ export function CreativeGallery({ status, className }: CreativeGalleryProps) {
101
111
  <Input aria-label="Primary text" value={editing.primary_text} onChange={(e) => setEditing({ ...editing, primary_text: e.target.value })} />
102
112
  <div className="flex gap-1"><Button size="sm" loading={busy === c.id} onClick={saveCopy}>Save</Button><Button size="sm" variant="ghost" onClick={() => setEditing(null)}>Cancel</Button></div>
103
113
  </div>
104
- ) : <div><p className="text-sm font-medium">{c.headline || c.name}</p><p className="line-clamp-2 text-sm text-muted-foreground">{c.primary_text}</p></div>}
114
+ ) : <div>{onSelectCreative ? <button type="button" className="text-left text-sm font-medium hover:underline" onClick={() => onSelectCreative(c)}>{c.headline || c.name}</button> : <p className="text-sm font-medium">{c.headline || c.name}</p>}<p className="line-clamp-2 text-sm text-muted-foreground">{c.primary_text}</p></div>}
105
115
  {c.status === "failed" && c.error_message ? <p className="text-xs text-destructive">{c.error_message}</p> : null}
106
116
  <div className="flex flex-wrap gap-1">
107
- <Button size="sm" variant="ghost" disabled={busy != null || c.status === "generating"} onClick={() => setEditing({ id: c.id, headline: c.headline ?? "", primary_text: c.primary_text ?? "" })}>Edit copy</Button>
117
+ <Button size="sm" variant="ghost" disabled={busy != null || c.status === "generating"} onClick={() => setEditing({ id: c.id, headline: c.headline ?? "", primary_text: c.primary_text ?? "", orig: { headline: c.headline ?? "", primary_text: c.primary_text ?? "" } })}>Edit copy</Button>
108
118
  <Button size="sm" variant="ghost" loading={busy === c.id} disabled={busy != null || c.status === "generating"} onClick={() => setPending({ kind: "regenerate", creative: c })}>New image</Button>
109
119
  {c.status !== "gallery" && c.status !== "generating" ? <Button size="sm" variant="outline" disabled={busy != null} onClick={() => void run(c.id, () => g8Post(`/ad-creatives/${c.id}/move-to-gallery`, {}))}>Move to gallery</Button> : null}
110
120
  <Button size="sm" variant="ghost" disabled={busy != null} onClick={() => setPending({ kind: "delete", creative: c })}>Delete</Button>
@@ -26,6 +26,7 @@ import { g8Patch, g8Post } from "@/g8/lib/g8-client";
26
26
  import { useSocialAccounts } from "../hooks/use-social-accounts";
27
27
  import { formatInstant, isFuture, localInputToIso } from "../lib/social-ads-types";
28
28
  import type { PostVariant, SocialPost } from "../lib/social-ads-types";
29
+ import { useBrand } from "@/g8/hooks/use-brand";
29
30
 
30
31
  export interface PostComposerProps {
31
32
  /** Called after a post was saved, scheduled or published. */
@@ -37,6 +38,7 @@ type Action = "schedule" | "publish";
37
38
  const newKey = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
38
39
 
39
40
  export function PostComposer({ onSaved, className }: PostComposerProps) {
41
+ const brand = useBrand();
40
42
  const { accounts, loading, error, refetch } = useSocialAccounts();
41
43
  const [account, setAccount] = useState("");
42
44
  const [body, setBody] = useState("");
@@ -76,7 +78,7 @@ export function PostComposer({ onSaved, className }: PostComposerProps) {
76
78
  });
77
79
 
78
80
  if (error) return <ApiError error={error} title="Could not load social accounts" onRetry={() => void refetch()} />;
79
- if (!loading && accounts.length === 0) return <EmptyState title="No social account connected" description="Connect a LinkedIn account in graph8 (Settings, Channels), then come back. It cannot be done through the API." />;
81
+ if (!loading && accounts.length === 0) return <EmptyState title="No social account connected" description={`Connect a LinkedIn account in ${brand.linkLabel} (Settings, Channels), then come back. It cannot be done through the API.`} />;
80
82
 
81
83
  const ready = body.trim().length > 0 && Boolean(chosen);
82
84
  const dialog = confirm === "publish"
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PostDetail: the complete screen for one social post. Loads the post by id (so `?id=` deep links work), shows every field
5
+ * (body, media, account, status, schedule, publish time, failure reason, link), and offers every action that fits its state:
6
+ * edit the draft, schedule, publish now, cancel a scheduled post, delete.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getSocialPost GET /social/posts/{post_id} (read)
10
+ * 2. updateSocialPost PATCH /social/posts/{post_id} (write; draft body)
11
+ * 3. scheduleSocialPost POST /social/posts/{post_id}/schedule (external, confirmed)
12
+ * 4. publishSocialPost POST /social/posts/{post_id}/publish (external, confirmed)
13
+ * 5. cancelSocialPost POST /social/posts/{post_id}/cancel (destructive, confirmed)
14
+ * 6. deleteSocialPost DELETE /social/posts/{post_id} (destructive, confirmed; 404 counts as done)
15
+ * Times are instants shown in the viewer's zone with its name.
16
+ */
17
+ import { useState } from "react";
18
+ import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
23
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
24
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
25
+ import { SectionCard } from "@/g8/components/screen/section-card";
26
+ import { StatusPill } from "@/g8/components/screen/status-pill";
27
+ import { formatInstant, isFuture, localInputToIso } from "../lib/social-ads-types";
28
+ import type { SocialPost } from "../lib/social-ads-types";
29
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
30
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
31
+
32
+ export interface PostDetailProps {
33
+ postId: string;
34
+ onClose?: () => void;
35
+ onExpand?: () => void;
36
+ onCollapse?: () => void;
37
+ /** Called after any change (edit, publish, schedule, cancel, delete) so the list can reload. */
38
+ onChanged?: () => void;
39
+ /** Called after the post was deleted. */
40
+ onDeleted?: () => void;
41
+ }
42
+
43
+ const tone = (s: string) => (s === "live" ? "positive" : s === "failed" ? "negative" : s === "scheduled" ? "info" : "neutral") as "positive" | "negative" | "info" | "neutral";
44
+
45
+ export function PostDetail({ postId, onClose, onExpand, onCollapse, onChanged, onDeleted }: PostDetailProps) {
46
+ const { ask, dialog, failure } = useConfirmAction();
47
+ const query = useG8Query(async (signal) => (await g8Get<SocialPost>(`/social/posts/${postId}`, undefined, { signal })).data, [postId]);
48
+ const [body, setBody] = useState<string | null>(null);
49
+ const [when, setWhen] = useState("");
50
+ const save = useG8Mutation((text: string) => g8Patch(`/social/posts/${postId}`, { body: text }), {
51
+ onSuccess: () => {
52
+ setBody(null);
53
+ void query.refetch();
54
+ onChanged?.();
55
+ },
56
+ });
57
+ const post = query.data;
58
+ const changed = () => {
59
+ void query.refetch();
60
+ onChanged?.();
61
+ };
62
+
63
+ if (query.error) return <ApiError error={query.error} title="Could not load the post" onRetry={() => void query.refetch()} />;
64
+ if (!post) return <Skeleton className="h-40 w-full" />;
65
+ const editable = ["draft", "ready", "failed"].includes(post.status);
66
+ const preview = post.body.length > 60 ? `${post.body.slice(0, 60)}...` : post.body;
67
+
68
+ const actions = (
69
+ <div className="flex flex-wrap gap-1">
70
+ {editable ? (
71
+ <AskButton ask={ask} label="Publish now" tier="external" title={`Publish: "${preview}"`} confirmLabel="Publish"
72
+ description="The post goes live now on the connected account and cannot be recalled from here."
73
+ run={() => g8Post(`/social/posts/${postId}/publish`, {})} onDone={changed} />
74
+ ) : null}
75
+ {post.status === "scheduled" ? (
76
+ <AskButton ask={ask} label="Cancel schedule" tier="destructive" title={`Cancel: "${preview}"`} confirmLabel="Cancel post"
77
+ description="The scheduled post will not be published. It stays as a cancelled record."
78
+ run={() => g8Post(`/social/posts/${postId}/cancel`, {})} onDone={changed} />
79
+ ) : null}
80
+ {post.status !== "publishing" ? (
81
+ <AskButton ask={ask} label="Delete" variant="ghost" tier="destructive" title={`Delete: "${preview}"`} confirmLabel="Delete"
82
+ description="The post is removed. A post that is already live stays live on the network."
83
+ tolerateStatus={[404]} run={() => g8Delete(`/social/posts/${postId}`)} onDone={() => { onChanged?.(); onDeleted?.(); onClose?.(); }} />
84
+ ) : null}
85
+ </div>
86
+ );
87
+
88
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
89
+ const tabs = [
90
+ {
91
+ id: "post",
92
+ label: "Post",
93
+ content: (
94
+ <div className="space-y-4">
95
+ <SectionCard title="Content">
96
+ {body == null ? (
97
+ <p className="whitespace-pre-wrap text-sm">{post.body}</p>
98
+ ) : (
99
+ <div className="space-y-2">
100
+ <Textarea aria-label="Post body" rows={8} value={body} onChange={(e) => setBody(e.target.value)} />
101
+ <div className="flex gap-2">
102
+ <Button size="sm" disabled={!body.trim() || save.loading} onClick={() => void save.mutate(body.trim())}>Save draft</Button>
103
+ <Button size="sm" variant="ghost" onClick={() => setBody(null)}>Cancel</Button>
104
+ </div>
105
+ {save.error ? <ApiError error={save.error} title="Could not save" /> : null}
106
+ </div>
107
+ )}
108
+ {editable && body == null ? <Button size="sm" variant="outline" className="mt-3" onClick={() => setBody(post.body)}>Edit text</Button> : null}
109
+ {(post.media ?? []).length > 0 ? (
110
+ <ul className="mt-3 grid grid-cols-2 gap-2">
111
+ {(post.media ?? []).map((m) => (
112
+ <li key={m.url}><img src={m.url} alt={m.alt ?? ""} className="aspect-video w-full rounded object-cover" /></li>
113
+ ))}
114
+ </ul>
115
+ ) : null}
116
+ </SectionCard>
117
+ <KeyValueList columns={2} items={[
118
+ { label: "Type", value: post.post_type },
119
+ { label: "Network", value: post.provider ?? "-" },
120
+ { label: "Account", value: post.account_ref ?? "-" },
121
+ { label: "Created", value: post.created_at ? formatInstant(post.created_at) : "-" },
122
+ { label: "Scheduled for", value: post.scheduled_at ? formatInstant(post.scheduled_at) : "-" },
123
+ { label: "Published", value: post.published_at ? formatInstant(post.published_at) : "-" },
124
+ ]} />
125
+ {post.failure_reason ? <p role="alert" className="text-sm text-error-foreground">{post.failure_reason}</p> : null}
126
+ {post.external_post_url ? <a className="text-sm underline" href={post.external_post_url} target="_blank" rel="noreferrer">View on the network</a> : null}
127
+ </div>
128
+ ),
129
+ },
130
+ ...(editable || post.status === "scheduled"
131
+ ? [{
132
+ id: "schedule",
133
+ label: "Schedule",
134
+ content: (
135
+ <SectionCard title="Schedule this post" description={`Pick a time in your zone (${zone}).`}>
136
+ <div className="flex flex-wrap items-center gap-2">
137
+ <Input type="datetime-local" aria-label="Publish at" value={when} onChange={(e) => setWhen(e.target.value)} className="w-56" />
138
+ {when && isFuture(when) ? (
139
+ <AskButton ask={ask} label="Schedule" tier="external" title={`Schedule: "${preview}"`} confirmLabel="Schedule"
140
+ description={`Publishes at ${formatInstant(localInputToIso(when))}.`}
141
+ run={() => g8Post(`/social/posts/${postId}/schedule`, { scheduled_at: localInputToIso(when) })} onDone={() => { setWhen(""); changed(); }} />
142
+ ) : (
143
+ <span className="text-xs text-muted-foreground">Choose a time at least a minute from now.</span>
144
+ )}
145
+ </div>
146
+ </SectionCard>
147
+ ),
148
+ }]
149
+ : []),
150
+ ];
151
+
152
+ return (
153
+ <>
154
+ <DetailPanel title={preview || "Post"} subtitle={post.provider ?? undefined} badges={<StatusPill tone={tone(post.status)}>{post.status}</StatusPill>}
155
+ actions={actions} tabs={tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
156
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
157
+ <ConfirmSpendDialog {...dialog} />
158
+ </>
159
+ );
160
+ }
@@ -28,6 +28,10 @@ export interface SocialPostListProps {
28
28
  /** Change to reload the list, for example after PostComposer saved a post. */
29
29
  refreshKey?: number;
30
30
  pageSize?: number;
31
+ /** Called when a person opens a post (show a PostDetail). */
32
+ onSelectPost?: (post: SocialPost) => void;
33
+ /** Highlights the open post. */
34
+ selectedId?: string | null;
31
35
  className?: string;
32
36
  }
33
37
 
@@ -39,7 +43,7 @@ const COPY: Record<Pending["kind"], { tier: ConfirmTier; verb: string; text: str
39
43
  delete: { tier: "destructive", verb: "Delete", text: "The post is removed. A post that is already live stays live on the network." },
40
44
  };
41
45
 
42
- export function SocialPostList({ refreshKey = 0, pageSize = 20, className }: SocialPostListProps) {
46
+ export function SocialPostList({ refreshKey = 0, pageSize = 20, onSelectPost, selectedId, className }: SocialPostListProps) {
43
47
  const [status, setStatus] = useState<(typeof STATUSES)[number]>("all");
44
48
  const [page, setPage] = useState(1);
45
49
  const [pending, setPending] = useState<Pending | null>(null);
@@ -87,9 +91,13 @@ export function SocialPostList({ refreshKey = 0, pageSize = 20, className }: Soc
87
91
  {!query.loading && !query.error && items.length === 0 ? <EmptyState title="No posts here" description="Write one with the composer." /> : null}
88
92
  <ul className="flex flex-col gap-2">
89
93
  {items.map((p) => (
90
- <li key={p.id} className="flex flex-wrap items-start justify-between gap-2 rounded-md border p-3" data-testid="post">
94
+ <li key={p.id} className={`flex flex-wrap items-start justify-between gap-2 rounded-md border p-3 ${selectedId === p.id ? "bg-muted/50" : ""}`} data-testid="post">
91
95
  <div className="flex min-w-0 flex-1 flex-col gap-1">
92
- <p className="line-clamp-3 text-sm">{p.body}</p>
96
+ {onSelectPost ? (
97
+ <button type="button" className="line-clamp-3 text-left text-sm hover:underline" onClick={() => onSelectPost(p)}>{p.body}</button>
98
+ ) : (
99
+ <p className="line-clamp-3 text-sm">{p.body}</p>
100
+ )}
93
101
  <p className="text-xs text-muted-foreground">
94
102
  {p.status}
95
103
  {p.status === "scheduled" && p.scheduled_at ? ` for ${formatInstant(p.scheduled_at)}` : ""}
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SocialScreen: the finished social publishing page. Two tabs:
5
+ * Posts every post by status (SocialPostList): publish a draft, cancel a scheduled post, delete. "New post" opens the
6
+ * composer in a side sheet (PostComposer): write or draft with AI, save, schedule or publish.
7
+ * Accounts the connected social accounts with their publishing capacity. Connecting an account (LinkedIn sign-in) only
8
+ * works in the graph8 app, so this tab links out instead of offering a dead button.
9
+ *
10
+ * Every post opens a complete detail screen (PostDetail, `?id=`, full page with `?view=full`).
11
+ *
12
+ * SDK operations: one read of listSocialPosts (page_size 1) for the per-status counts in the stat strip. Composes
13
+ * PostComposer (billable AI drafts, external schedule/publish, all confirmed), SocialPostList (external + destructive,
14
+ * confirmed) and useSocialAccounts (read).
15
+ */
16
+ import { useState } from "react";
17
+ import type { ReactNode } from "react";
18
+ import { Button } from "@graph8/sdk/ui";
19
+ import { Megaphone, Plus } from "lucide-react";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { useUrlState } from "@/g8/hooks/use-url-state";
24
+ import { g8Get } from "@/g8/lib/g8-client";
25
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
26
+ import type { StatItem } from "@/g8/components/screen/stat-strip";
27
+ import { SplitView } from "@/g8/components/screen/split-view";
28
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
29
+ import { SectionCard } from "@/g8/components/screen/section-card";
30
+ import { StatusPill } from "@/g8/components/screen/status-pill";
31
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
32
+ import { useSocialAccounts } from "../hooks/use-social-accounts";
33
+ import type { SocialPostPage } from "../lib/social-ads-types";
34
+ import { PostComposer } from "./post-composer";
35
+ import { SocialPostList } from "./social-post-list";
36
+ import { PostDetail } from "./post-detail";
37
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
38
+
39
+ export interface SocialScreenProps {
40
+ /** Where "Connect" sends people. Default: graph8 Settings > Integrations. */
41
+ connectUrl?: string;
42
+ toolbarActions?: ReactNode;
43
+ className?: string;
44
+ }
45
+
46
+ /** Integrations settings on the platform's web app, resolved against the brand. */
47
+ const APP_PATH = "/studio/settings?tab=integrations";
48
+
49
+ function Accounts({ connectUrl }: { connectUrl: string }) {
50
+ const brand = useBrand();
51
+ const { accounts, loading, error, refetch } = useSocialAccounts();
52
+ if (error) return <ApiError error={error} onRetry={() => void refetch()} />;
53
+ if (loading && accounts.length === 0) return <p className="text-sm text-muted-foreground" role="status">Loading accounts...</p>;
54
+ if (accounts.length === 0) {
55
+ return (
56
+ <EmptyState
57
+ title="No social account connected"
58
+ description={`Sign in with LinkedIn in ${brand.linkLabel} to publish from here.`}
59
+ action={<Button asChild><a href={connectUrl} target="_blank" rel="noreferrer">Connect in {brand.linkLabel}</a></Button>}
60
+ />
61
+ );
62
+ }
63
+ return (
64
+ <ul className="grid gap-3 md:grid-cols-2">
65
+ {accounts.map((a) => (
66
+ <li key={a.id}>
67
+ <SectionCard title={a.display_name ?? a.provider} description={a.provider}>
68
+ <div className="flex items-center gap-3">
69
+ <EntityAvatar name={a.display_name ?? a.provider} kind="person" />
70
+ <div className="min-w-0 flex-1 text-sm">
71
+ <StatusPill tone={a.status === "active" || a.status === "connected" ? "positive" : "warning"}>{a.status ?? "unknown"}</StatusPill>
72
+ <p className="mt-1 text-xs text-muted-foreground tabular-nums">
73
+ {a.capacity.publish_used} of {a.capacity.publish_limit} posts used
74
+ </p>
75
+ </div>
76
+ {a.profile_url ? <a className="text-sm underline" href={a.profile_url} target="_blank" rel="noreferrer">Profile</a> : null}
77
+ </div>
78
+ </SectionCard>
79
+ </li>
80
+ ))}
81
+ </ul>
82
+ );
83
+ }
84
+
85
+ export function SocialScreen(props: SocialScreenProps) {
86
+ const brand = useBrand();
87
+ const connectUrl = props.connectUrl ?? brandUrl(brand, APP_PATH);
88
+ const [tabValue, setTab] = useUrlState("tab", "posts");
89
+ const tab = tabValue === "accounts" ? "accounts" : "posts";
90
+ const [id, setId] = useUrlState("id", "");
91
+ const [view, setView] = useUrlState("view", "");
92
+ const expanded = view === "full" && Boolean(id);
93
+ const [composing, setComposing] = useState(false);
94
+ const [refreshKey, setRefreshKey] = useState(0);
95
+ const counts = useG8Query(async (signal) => (await g8Get<SocialPostPage>("/social/posts", { page: 1, page_size: 1 }, { signal })).data, [refreshKey]);
96
+
97
+ const c = counts.data?.counts ?? {};
98
+ const stats: StatItem[] | undefined = counts.data
99
+ ? [
100
+ { label: "Live", value: c.live ?? 0, tone: "positive" },
101
+ { label: "Scheduled", value: c.scheduled ?? 0, tone: "info" },
102
+ { label: "Drafts", value: c.draft ?? 0 },
103
+ { label: "Failed", value: c.failed ?? 0, tone: c.failed ? "negative" : "neutral" },
104
+ ]
105
+ : undefined;
106
+
107
+ return (
108
+ <ScreenLayout
109
+ title="Social"
110
+ description="Write, schedule and publish posts, and see what is live."
111
+ icon={<Megaphone className="size-5" />}
112
+ actions={
113
+ <>
114
+ {props.toolbarActions}
115
+ <Button onClick={() => setComposing(true)}>
116
+ <Plus className="mr-1.5 size-4" /> New post
117
+ </Button>
118
+ </>
119
+ }
120
+ tabs={[
121
+ { id: "posts", label: "Posts" },
122
+ { id: "accounts", label: "Accounts" },
123
+ ]}
124
+ activeTab={tab}
125
+ onTabChange={(next) => {
126
+ setTab(next);
127
+ setId("");
128
+ setView("");
129
+ }}
130
+ stats={tab === "posts" ? stats : undefined}
131
+ className={props.className}
132
+ >
133
+ {tab === "posts" ? (
134
+ <SplitView
135
+ detailOpen={Boolean(id)}
136
+ onCloseDetail={() => { setId(""); setView(""); }}
137
+ expanded={expanded}
138
+ onExpandedChange={(next) => setView(next ? "full" : "")}
139
+ detailWidth="lg"
140
+ list={<SocialPostList refreshKey={refreshKey} selectedId={id} onSelectPost={(p) => setId(p.id)} />}
141
+ detail={id ? (
142
+ <PostDetail key={id} postId={id} onClose={() => { setId(""); setView(""); }}
143
+ onExpand={expanded ? undefined : () => setView("full")} onCollapse={expanded ? () => setView("") : undefined}
144
+ onChanged={() => setRefreshKey((k) => k + 1)} />
145
+ ) : null}
146
+ />
147
+ ) : (
148
+ <Accounts connectUrl={connectUrl} />
149
+ )}
150
+ <FormSheet open={composing} onOpenChange={setComposing} title="New post" description="Write it, draft it with AI, then save, schedule or publish.">
151
+ <PostComposer
152
+ onSaved={() => {
153
+ setComposing(false);
154
+ setRefreshKey((k) => k + 1);
155
+ }}
156
+ />
157
+ </FormSheet>
158
+ </ScreenLayout>
159
+ );
160
+ }
@@ -5,6 +5,16 @@ and chart traffic. The Apollo / 6sense "website visitors" screen.
5
5
 
6
6
  Depends on: `crm`. Install: `npx @graph8/sdk add visitors`.
7
7
 
8
+ ## Screens
9
+
10
+ `VisitorsScreen` (`components/visitors-screen.tsx`) is the finished page: copy it as your route. No props are required.
11
+
12
+ - **Visitors tab**: bots/internal-traffic toggles, 7-day stat strip (visitors, sessions, page views with sparklines), the polled company/people feed, and a detail
13
+ panel for a clicked person with the pages they viewed. Company cards expand to the full journey.
14
+ - **Traffic tab**: 7/30/90 day window, trend chart, top pages, "on the site now". **Install tab**: the snippet and an install check.
15
+ - Slots: `toolbarActions`, `renderDetailActions(visit, kind)`, `extraDetailTabs` (each `{ id, label, render(visit, kind) }`; shown once the visitor row is known). Everything is read tier.
16
+ - Prerequisite: the snippet pasted into your site (only a person can do this); People also needs `contacts:read`.
17
+
8
18
  ## Components
9
19
 
10
20
  | Component | What it does | Tiers |
@@ -71,6 +71,29 @@
71
71
  "tiers": [
72
72
  "read"
73
73
  ]
74
+ },
75
+ {
76
+ "name": "VisitorsScreen",
77
+ "file": "components/visitors-screen.tsx",
78
+ "purpose": "Finished website-visitors page: week stat strip, company and people feed with person detail, traffic charts and the snippet install check.",
79
+ "operations": [
80
+ "get_audience_trend_analytics_audience_trend_get"
81
+ ],
82
+ "tiers": [
83
+ "read"
84
+ ]
85
+ },
86
+ {
87
+ "name": "VisitorDetail",
88
+ "file": "components/visitor-detail.tsx",
89
+ "purpose": "Complete screen for one visiting company or identified person: fields, pages viewed, full journey.",
90
+ "operations": [
91
+ "list_company_visitor_events_visitors_companies_events_get",
92
+ "list_contact_visitor_events_visitors_contacts_events_get"
93
+ ],
94
+ "tiers": [
95
+ "read"
96
+ ]
74
97
  }
75
98
  ],
76
99
  "hooks": [],
@@ -134,6 +157,7 @@
134
157
  }
135
158
  ],
136
159
  "peerDependencies": {
137
- "react": ">=18"
160
+ "react": ">=18",
161
+ "lucide-react": ">=0.400"
138
162
  }
139
163
  }