@graph8/sdk 0.370.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (466) hide show
  1. package/README.md +19 -0
  2. package/blocks/_foundation/README.md +14 -0
  3. package/blocks/_foundation/block.json +44 -0
  4. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  5. package/blocks/_foundation/components/empty-state.tsx +5 -2
  6. package/blocks/_foundation/components/list-select.tsx +112 -0
  7. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  8. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  9. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  10. package/blocks/_foundation/components/screen/index.ts +1 -0
  11. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  12. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  13. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  14. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  15. package/blocks/_foundation/lib/paging.ts +57 -0
  16. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  17. package/blocks/agents/README.md +81 -4
  18. package/blocks/agents/block.json +311 -2
  19. package/blocks/agents/components/agent-activity.tsx +112 -0
  20. package/blocks/agents/components/agent-approvals.tsx +135 -0
  21. package/blocks/agents/components/agent-builder.tsx +17 -3
  22. package/blocks/agents/components/agent-conversations.tsx +143 -0
  23. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  24. package/blocks/agents/components/agent-operators.tsx +164 -0
  25. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  26. package/blocks/agents/components/agent-runs.tsx +3 -3
  27. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  28. package/blocks/agents/components/agent-test-history.tsx +121 -0
  29. package/blocks/agents/components/agents-screen.tsx +53 -11
  30. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  31. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  32. package/blocks/agents/components/from-template-button.tsx +65 -0
  33. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  34. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  35. package/blocks/agents/components/knowledge-form.tsx +69 -0
  36. package/blocks/agents/components/name-dialog.tsx +32 -0
  37. package/blocks/agents/components/receptionist-button.tsx +46 -0
  38. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  39. package/blocks/agents/components/skill-editor.tsx +147 -0
  40. package/blocks/agents/components/skill-library.tsx +111 -0
  41. package/blocks/agents/lib/agent-types.ts +7 -1
  42. package/blocks/agents/lib/facts.tsx +13 -0
  43. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  44. package/blocks/agents/lib/load-more.tsx +141 -0
  45. package/blocks/agents/lib/skill-utils.ts +91 -0
  46. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  47. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  48. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  49. package/blocks/analytics/components/report-builder.tsx +15 -2
  50. package/blocks/appointments/README.md +6 -0
  51. package/blocks/appointments/block.json +27 -0
  52. package/blocks/appointments/components/booking-page.tsx +2 -2
  53. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  54. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  55. package/blocks/appointments/components/meeting-list.tsx +29 -13
  56. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  57. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  58. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  59. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  60. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  61. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  62. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  63. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  64. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  65. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  66. package/blocks/coach/components/call-scorecard.tsx +7 -7
  67. package/blocks/company-intelligence/README.md +132 -0
  68. package/blocks/company-intelligence/block.json +318 -0
  69. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  70. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  71. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  72. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  73. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  74. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  75. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  76. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  77. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  78. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  79. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  80. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  81. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  82. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  83. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  84. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  85. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  86. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  87. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  88. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  89. package/blocks/crm/README.md +6 -0
  90. package/blocks/crm/block.json +53 -4
  91. package/blocks/crm/components/companies-screen.tsx +23 -9
  92. package/blocks/crm/components/company-detail.tsx +2 -2
  93. package/blocks/crm/components/contact-detail.tsx +4 -4
  94. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  95. package/blocks/crm/components/contacts-screen.tsx +31 -13
  96. package/blocks/crm/components/dedupe-review.tsx +6 -1
  97. package/blocks/crm/components/import-wizard.tsx +3 -7
  98. package/blocks/crm/components/list-select.tsx +20 -0
  99. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  100. package/blocks/crm/components/record-form.tsx +1 -1
  101. package/blocks/crm/components/tasks-screen.tsx +35 -15
  102. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  103. package/blocks/crm/hooks/use-page-query.ts +3 -1
  104. package/blocks/crm/lib/crm-types.ts +14 -0
  105. package/blocks/crm/lib/read-lists.ts +16 -0
  106. package/blocks/deals/README.md +24 -6
  107. package/blocks/deals/block.json +151 -2
  108. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  109. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  110. package/blocks/deals/components/deal-detail.tsx +2 -2
  111. package/blocks/deals/components/deal-filters.tsx +65 -0
  112. package/blocks/deals/components/deal-line-items.tsx +120 -0
  113. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  114. package/blocks/deals/components/deal-related.tsx +30 -9
  115. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  116. package/blocks/deals/components/deals-screen.tsx +57 -22
  117. package/blocks/deals/components/forecast-view.tsx +4 -4
  118. package/blocks/deals/components/pipeline-board.tsx +8 -6
  119. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  120. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  121. package/blocks/deals/components/stage-editor.tsx +1 -1
  122. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  123. package/blocks/deals/lib/deal-extras.ts +156 -0
  124. package/blocks/dialer/README.md +14 -5
  125. package/blocks/dialer/block.json +17 -2
  126. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  127. package/blocks/dialer/components/call-log-screen.tsx +4 -1
  128. package/blocks/dialer/components/call-log.tsx +16 -4
  129. package/blocks/dialer/components/call-queue.tsx +3 -3
  130. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  131. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  132. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  133. package/blocks/dialer/components/session-start-form.tsx +58 -11
  134. package/blocks/dialer/components/softphone.tsx +1 -1
  135. package/blocks/enrichment/README.md +16 -6
  136. package/blocks/enrichment/block.json +19 -7
  137. package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
  138. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  139. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  140. package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
  141. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  142. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  143. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  144. package/blocks/forms/README.md +8 -0
  145. package/blocks/forms/block.json +107 -0
  146. package/blocks/forms/components/form-activity.tsx +71 -0
  147. package/blocks/forms/components/form-backfill.tsx +108 -0
  148. package/blocks/forms/components/form-builder.tsx +1 -1
  149. package/blocks/forms/components/form-embed.tsx +1 -1
  150. package/blocks/forms/components/form-groups.tsx +151 -0
  151. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  152. package/blocks/forms/components/form-route-history.tsx +72 -0
  153. package/blocks/forms/components/form-saved-views.tsx +81 -0
  154. package/blocks/forms/components/forms-screen.tsx +51 -17
  155. package/blocks/forms/components/more-rows.tsx +30 -0
  156. package/blocks/forms/components/submissions-export.tsx +67 -0
  157. package/blocks/forms/components/submissions-table.tsx +15 -6
  158. package/blocks/forms/lib/forms-extra.ts +67 -0
  159. package/blocks/inbox/README.md +5 -0
  160. package/blocks/inbox/block.json +15 -0
  161. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  162. package/blocks/inbox/components/thread-badge.tsx +2 -2
  163. package/blocks/inbox/components/thread-view.tsx +1 -1
  164. package/blocks/integrations/README.md +54 -1
  165. package/blocks/integrations/block.json +189 -15
  166. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  167. package/blocks/integrations/components/connection-health.tsx +59 -0
  168. package/blocks/integrations/components/crm-detail.tsx +20 -1
  169. package/blocks/integrations/components/crm-records.tsx +103 -0
  170. package/blocks/integrations/components/field-mappings.tsx +61 -0
  171. package/blocks/integrations/components/integration-list.tsx +4 -4
  172. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  173. package/blocks/integrations/components/migration-panel.tsx +133 -0
  174. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  175. package/blocks/integrations/components/sync-status.tsx +6 -6
  176. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  177. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  178. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  179. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  180. package/blocks/integrations/lib/crm-utils.ts +39 -0
  181. package/blocks/integrations/lib/facts.tsx +13 -0
  182. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  183. package/blocks/integrations/lib/load-more.tsx +141 -0
  184. package/blocks/intent/components/account-intent-score.tsx +5 -5
  185. package/blocks/intent/components/intent-feed.tsx +8 -8
  186. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  187. package/blocks/intent/lib/intent-utils.ts +4 -4
  188. package/blocks/lifecycle/components/account-related.tsx +8 -1
  189. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  190. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  191. package/blocks/lists/README.md +7 -1
  192. package/blocks/lists/block.json +31 -3
  193. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  194. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  195. package/blocks/lists/components/list-picker.tsx +13 -3
  196. package/blocks/lists/components/list-table.tsx +1 -1
  197. package/blocks/lists/components/lists-screen.tsx +4 -2
  198. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  199. package/blocks/lists/components/suppression-manager.tsx +27 -6
  200. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  201. package/blocks/mailboxes/block.json +4 -0
  202. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  203. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  204. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  205. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  206. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  207. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  208. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  209. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  210. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  211. package/blocks/newsletter/README.md +12 -0
  212. package/blocks/newsletter/block.json +117 -0
  213. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  214. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  215. package/blocks/newsletter/components/more-rows.tsx +30 -0
  216. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  217. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  218. package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
  219. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  220. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  221. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  222. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  223. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  224. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  225. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  226. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  227. package/blocks/nurture/components/nurture-form.tsx +5 -8
  228. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  229. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  230. package/blocks/objects/README.md +1 -1
  231. package/blocks/objects/block.json +13 -5
  232. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  233. package/blocks/objects/components/object-record-detail.tsx +8 -1
  234. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  235. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  236. package/blocks/pages/README.md +14 -0
  237. package/blocks/pages/block.json +113 -1
  238. package/blocks/pages/components/more-rows.tsx +30 -0
  239. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  240. package/blocks/pages/components/page-analytics.tsx +176 -0
  241. package/blocks/pages/components/page-clone.tsx +99 -0
  242. package/blocks/pages/components/page-domains.tsx +167 -0
  243. package/blocks/pages/components/page-editor.tsx +1 -1
  244. package/blocks/pages/components/page-generation-status.tsx +48 -0
  245. package/blocks/pages/components/page-tracking.tsx +137 -0
  246. package/blocks/pages/components/pages-screen.tsx +57 -16
  247. package/blocks/pages/components/seo-report.tsx +6 -1
  248. package/blocks/pages/lib/studio.ts +7 -0
  249. package/blocks/phone-numbers/README.md +74 -0
  250. package/blocks/phone-numbers/block.json +262 -0
  251. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  252. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  253. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  254. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  255. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  256. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  257. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  258. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  259. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  260. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  261. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  262. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  263. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  264. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  265. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  266. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  267. package/blocks/pickers/README.md +23 -0
  268. package/blocks/pickers/block.json +103 -0
  269. package/blocks/pickers/components/company-picker.tsx +90 -0
  270. package/blocks/pickers/components/contact-picker.tsx +131 -0
  271. package/blocks/pickers/components/member-picker.tsx +99 -0
  272. package/blocks/pickers/components/search-picker.tsx +98 -0
  273. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  274. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  275. package/blocks/pickers/lib/use-debounced.ts +13 -0
  276. package/blocks/quotes/README.md +13 -0
  277. package/blocks/quotes/block.json +48 -1
  278. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  279. package/blocks/quotes/components/quote-builder.tsx +1 -1
  280. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  281. package/blocks/quotes/components/quote-list.tsx +5 -3
  282. package/blocks/quotes/components/quote-related.tsx +3 -2
  283. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  284. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  285. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  286. package/blocks/radar/components/radar-overview.tsx +4 -4
  287. package/blocks/radar/components/radar-screen.tsx +2 -2
  288. package/blocks/sequencer/README.md +6 -0
  289. package/blocks/sequencer/block.json +28 -1
  290. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  291. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  292. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  293. package/blocks/sequencer/components/sequence-builder.tsx +19 -20
  294. package/blocks/sequencer/components/sequence-detail.tsx +4 -2
  295. package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
  296. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  297. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  298. package/blocks/sequencer/components/sequences-screen.tsx +3 -0
  299. package/blocks/sequencer/components/step-editor.tsx +11 -11
  300. package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
  301. package/blocks/social-ads/README.md +8 -0
  302. package/blocks/social-ads/block.json +137 -4
  303. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  304. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  305. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  306. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  307. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  308. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  309. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  310. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  311. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  312. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  313. package/blocks/social-ads/components/post-composer.tsx +4 -2
  314. package/blocks/social-ads/components/post-detail.tsx +3 -0
  315. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  316. package/blocks/social-ads/components/social-performance.tsx +80 -0
  317. package/blocks/social-ads/components/social-screen.tsx +33 -6
  318. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  319. package/blocks/surveys/README.md +91 -0
  320. package/blocks/surveys/block.json +299 -0
  321. package/blocks/surveys/components/account-nps.tsx +69 -0
  322. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  323. package/blocks/surveys/components/org-summary.tsx +82 -0
  324. package/blocks/surveys/components/sending-domains.tsx +113 -0
  325. package/blocks/surveys/components/survey-form.tsx +126 -0
  326. package/blocks/surveys/components/survey-responses.tsx +145 -0
  327. package/blocks/surveys/components/survey-results.tsx +72 -0
  328. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  329. package/blocks/surveys/components/survey-send.tsx +162 -0
  330. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  331. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  332. package/blocks/surveys/lib/surveys-types.ts +228 -0
  333. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  334. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  335. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  336. package/blocks/visitors/components/snippet-install.tsx +18 -9
  337. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  338. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  339. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  340. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  341. package/blocks/webchat/components/conversation-list.tsx +1 -1
  342. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  343. package/blocks/workbench/components/run-progress.tsx +1 -1
  344. package/blocks/workbench/components/workbench-grid.tsx +1 -1
  345. package/blocks/workbench/components/workbench-screen.tsx +15 -5
  346. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  347. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  348. package/dist/cli.js +536 -392
  349. package/dist/index.browser.js +1 -1
  350. package/dist/index.browser.js.map +1 -1
  351. package/dist/index.browser.mjs +1 -1
  352. package/dist/index.browser.mjs.map +1 -1
  353. package/dist/index.d.mts +2 -2
  354. package/dist/index.d.ts +2 -2
  355. package/dist/index.js +1 -1
  356. package/dist/index.js.map +1 -1
  357. package/dist/index.mjs +1 -1
  358. package/dist/index.mjs.map +1 -1
  359. package/dist/react.browser.js +1 -1
  360. package/dist/react.browser.js.map +1 -1
  361. package/dist/react.browser.mjs +1 -1
  362. package/dist/react.browser.mjs.map +1 -1
  363. package/dist/react.d.mts +2 -2
  364. package/dist/react.d.ts +2 -2
  365. package/dist/react.js +1 -1
  366. package/dist/react.js.map +1 -1
  367. package/dist/react.mjs +1 -1
  368. package/dist/react.mjs.map +1 -1
  369. package/dist/types/blocks/brand.d.ts +11 -2
  370. package/dist/types/blocks/create.d.ts +7 -3
  371. package/dist/types/blocks/doctor.d.ts +1 -0
  372. package/dist/types/blocks/flows.d.ts +7 -1
  373. package/dist/types/blocks/orgBrand.d.ts +34 -0
  374. package/dist/types/cli.d.ts +3 -0
  375. package/dist/types/version.d.ts +2 -2
  376. package/docs/api/desk.d.ts +2 -2
  377. package/docs/api/desk.md +3 -3
  378. package/package.json +1 -1
  379. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  380. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  381. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  382. package/stacks/6sense.json +17 -4
  383. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  384. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  385. package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
  386. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  387. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  388. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  389. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  390. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  391. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  392. package/stacks/apollo.json +36 -8
  393. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  394. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  395. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  396. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  397. package/stacks/calendly.json +8 -4
  398. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  399. package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
  400. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  401. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  402. package/stacks/clay.json +16 -3
  403. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  404. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  405. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  406. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  407. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  408. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  409. package/stacks/gong.json +23 -6
  410. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  411. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  412. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  413. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  414. package/stacks/hootsuite.json +9 -4
  415. package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
  416. package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
  417. package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
  418. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  419. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  420. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  421. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  422. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  423. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  424. package/stacks/hubspot.json +21 -5
  425. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  426. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  427. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  428. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  429. package/stacks/intercom.json +8 -4
  430. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  431. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  432. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  433. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  434. package/stacks/mailchimp.json +20 -5
  435. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  436. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  437. package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
  438. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  439. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  440. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  441. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  442. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  443. package/stacks/outreach.json +25 -7
  444. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  445. package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
  446. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  447. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  448. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  449. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  450. package/stacks/pipedrive.json +4 -2
  451. package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
  452. package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
  453. package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
  454. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  455. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  456. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  457. package/stacks/salesforce.json +7 -3
  458. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  459. package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
  460. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  461. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  462. package/stacks/zoominfo.json +16 -3
  463. package/styles/styles.css +1 -1
  464. package/templates/next-app/app/globals.css.tmpl +349 -64
  465. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  466. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -18,28 +18,51 @@
18
18
  "activate_newsletter_newsletters__newsletter_id__activate_post",
19
19
  "add_subscriber_newsletters__newsletter_id__subscribers_post",
20
20
  "cancel_send_newsletters__newsletter_id__issues__issue_id__send_cancel_post",
21
+ "complete_newsletter_newsletters__newsletter_id__complete_post",
21
22
  "create_issue_newsletters__newsletter_id__issues_post",
22
23
  "create_newsletter_newsletters_post",
24
+ "create_template_from_prompt_newsletters_templates_custom_post",
23
25
  "delete_issue_newsletters__newsletter_id__issues__issue_id__delete",
26
+ "delete_newsletter_template_newsletters_templates__template_id__delete",
24
27
  "delete_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__delete",
28
+ "disable_newsletter_list_sync_newsletters__newsletter_id__list_syncs__audience_list_id__delete",
29
+ "duplicate_issue_newsletters__newsletter_id__issues__issue_id__duplicate_post",
30
+ "enable_newsletter_list_sync_newsletters__newsletter_id__list_syncs_post",
25
31
  "generate_newsletter_issue_newsletters__newsletter_id__issues__issue_id__generate_post",
26
32
  "get_issue_analytics_kpis_newsletters__newsletter_id__issues__issue_id__analytics_kpis_get",
27
33
  "get_issue_generation_status_newsletters__newsletter_id__issues__issue_id__generation_status_get",
28
34
  "get_issue_newsletters__newsletter_id__issues__issue_id__get",
35
+ "get_newsletter_newsletters__newsletter_id__get",
36
+ "get_newsletter_settings_newsletters__newsletter_id__settings_get",
29
37
  "get_send_status_newsletters__newsletter_id__issues__issue_id__send_get",
38
+ "get_subscriber_timeline_newsletters__newsletter_id__issues__issue_id__analytics_subscribers__subscriber_id__timeline_get",
30
39
  "import_subscribers_from_audience_newsletters__newsletter_id__subscribers_import_audience_post",
31
40
  "list_issues_newsletters__newsletter_id__issues_get",
32
41
  "list_lists_lists_get",
42
+ "list_newsletter_issue_versions_newsletters__newsletter_id__issues__issue_id__versions_get",
43
+ "list_newsletter_list_syncs_newsletters__newsletter_id__list_syncs_get",
33
44
  "list_newsletters_newsletters_get",
34
45
  "list_subscribers_newsletters__newsletter_id__subscribers_get",
46
+ "list_templates_newsletters_templates_get",
47
+ "newsletter_domain_status_newsletters__newsletter_id__settings_domain_status_get",
35
48
  "pause_newsletter_newsletters__newsletter_id__pause_post",
36
49
  "pause_send_newsletters__newsletter_id__issues__issue_id__send_pause_post",
37
50
  "preview_newsletter_subscriber_verification_newsletters__newsletter_id__subscribers_verify_preview_post",
51
+ "restore_newsletter_issue_version_newsletters__newsletter_id__issues__issue_id__versions__version_num__restore_post",
52
+ "resume_newsletter_newsletters__newsletter_id__resume_post",
38
53
  "resume_send_newsletters__newsletter_id__issues__issue_id__send_resume_post",
54
+ "run_newsletter_list_sync_newsletters__newsletter_id__list_syncs__audience_list_id__run_post",
39
55
  "send_issue_newsletters__newsletter_id__issues__issue_id__send_post",
40
56
  "send_test_email_newsletters__newsletter_id__issues__issue_id__send_test_post",
57
+ "set_template_approval_newsletters_templates__template_id__approval_post",
58
+ "template_generation_status_newsletters_templates__template_id__generation_status_get",
41
59
  "unsubscribe_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__unsubscribe_patch",
42
60
  "update_issue_newsletters__newsletter_id__issues__issue_id__put",
61
+ "update_newsletter_newsletters__newsletter_id__patch",
62
+ "update_newsletter_settings_newsletters__newsletter_id__settings_put",
63
+ "update_newsletter_template_newsletters_templates__template_id__patch",
64
+ "use_template_newsletters_templates__template_id__use_post",
65
+ "verify_newsletter_domain_newsletters__newsletter_id__settings_verify_domain_post",
43
66
  "verify_newsletter_subscribers_newsletters__newsletter_id__subscribers_verify_post"
44
67
  ],
45
68
  "components": [
@@ -155,6 +178,96 @@
155
178
  "tiers": [
156
179
  "read"
157
180
  ]
181
+ },
182
+ {
183
+ "name": "IssueVersions",
184
+ "file": "components/issue-versions.tsx",
185
+ "purpose": "Version history of one issue with restore, and duplicate into a new draft.",
186
+ "operations": [
187
+ "list_newsletter_issue_versions_newsletters__newsletter_id__issues__issue_id__versions_get",
188
+ "restore_newsletter_issue_version_newsletters__newsletter_id__issues__issue_id__versions__version_num__restore_post",
189
+ "duplicate_issue_newsletters__newsletter_id__issues__issue_id__duplicate_post"
190
+ ],
191
+ "tiers": [
192
+ "read",
193
+ "write",
194
+ "external"
195
+ ]
196
+ },
197
+ {
198
+ "name": "MoreRows",
199
+ "file": "components/more-rows.tsx",
200
+ "purpose": "Footer for paged lists: how many rows are shown and a Load more button.",
201
+ "operations": [],
202
+ "tiers": []
203
+ },
204
+ {
205
+ "name": "NewsletterListSyncs",
206
+ "file": "components/newsletter-list-syncs.tsx",
207
+ "purpose": "Keep subscribers in step with lists: turn a sync on (consent confirmed), run it, turn it off.",
208
+ "operations": [
209
+ "list_newsletter_list_syncs_newsletters__newsletter_id__list_syncs_get",
210
+ "list_lists_lists_get",
211
+ "enable_newsletter_list_sync_newsletters__newsletter_id__list_syncs_post",
212
+ "run_newsletter_list_sync_newsletters__newsletter_id__list_syncs__audience_list_id__run_post",
213
+ "disable_newsletter_list_sync_newsletters__newsletter_id__list_syncs__audience_list_id__delete"
214
+ ],
215
+ "tiers": [
216
+ "read",
217
+ "write",
218
+ "destructive"
219
+ ]
220
+ },
221
+ {
222
+ "name": "NewsletterSettings",
223
+ "file": "components/newsletter-settings.tsx",
224
+ "purpose": "The newsletter itself: name and goal, sender and opt-in, sending domain verification, resume and complete.",
225
+ "operations": [
226
+ "update_newsletter_newsletters__newsletter_id__patch",
227
+ "update_newsletter_settings_newsletters__newsletter_id__settings_put",
228
+ "newsletter_domain_status_newsletters__newsletter_id__settings_domain_status_get",
229
+ "verify_newsletter_domain_newsletters__newsletter_id__settings_verify_domain_post",
230
+ "complete_newsletter_newsletters__newsletter_id__complete_post",
231
+ "resume_newsletter_newsletters__newsletter_id__resume_post",
232
+ "get_newsletter_settings_newsletters__newsletter_id__settings_get",
233
+ "get_newsletter_newsletters__newsletter_id__get"
234
+ ],
235
+ "tiers": [
236
+ "read",
237
+ "write",
238
+ "external"
239
+ ]
240
+ },
241
+ {
242
+ "name": "NewsletterTemplates",
243
+ "file": "components/newsletter-templates.tsx",
244
+ "purpose": "Saved issue templates: start a draft from one, approve, rename, delete, or write one with AI.",
245
+ "operations": [
246
+ "update_newsletter_template_newsletters_templates__template_id__patch",
247
+ "set_template_approval_newsletters_templates__template_id__approval_post",
248
+ "use_template_newsletters_templates__template_id__use_post",
249
+ "template_generation_status_newsletters_templates__template_id__generation_status_get",
250
+ "delete_newsletter_template_newsletters_templates__template_id__delete",
251
+ "list_templates_newsletters_templates_get",
252
+ "create_template_from_prompt_newsletters_templates_custom_post"
253
+ ],
254
+ "tiers": [
255
+ "read",
256
+ "write",
257
+ "billable",
258
+ "destructive"
259
+ ]
260
+ },
261
+ {
262
+ "name": "SubscriberTimeline",
263
+ "file": "components/subscriber-timeline.tsx",
264
+ "purpose": "What one subscriber did with one sent issue.",
265
+ "operations": [
266
+ "get_subscriber_timeline_newsletters__newsletter_id__issues__issue_id__analytics_subscribers__subscriber_id__timeline_get"
267
+ ],
268
+ "tiers": [
269
+ "read"
270
+ ]
158
271
  }
159
272
  ],
160
273
  "hooks": [],
@@ -162,6 +275,10 @@
162
275
  {
163
276
  "file": "lib/newsletter-types.ts",
164
277
  "purpose": "Newsletter/issue/subscriber types and helpers: draft check, percentages, scheduled-time conversion, generation-in-progress guard."
278
+ },
279
+ {
280
+ "file": "lib/newsletter-extra.ts",
281
+ "purpose": "Helpers for the newsletter management components: open-dict readers, changed-fields diff, domain and DNS helpers."
165
282
  }
166
283
  ],
167
284
  "recipes": [
@@ -109,7 +109,7 @@ export function IssueScheduler({ newsletterId, issueId, onSent }: IssueScheduler
109
109
  <Card>
110
110
  <CardHeader><CardTitle>{issueTitle(issue.data)} <span className="text-muted-foreground text-sm font-normal">{issue.data.status}</span></CardTitle></CardHeader>
111
111
  <CardContent className="grid gap-4">
112
- {partial ? <p role="alert" className="text-sm text-amber-700">This issue is still being written by AI. Wait for it to finish: sending now would mail the partial version.</p> : null}
112
+ {partial ? <p role="alert" className="text-sm text-warning-foreground">This issue is still being written by AI. Wait for it to finish: sending now would mail the partial version.</p> : null}
113
113
  {draft ? (
114
114
  <div className="grid gap-3">
115
115
  <p className="text-sm">Audience: <strong>{audience.loading ? "..." : subscribers.toLocaleString()}</strong> subscribed {pluralWord(subscribers, "person", "people")}.</p>
@@ -118,7 +118,7 @@ export function IssueScheduler({ newsletterId, issueId, onSent }: IssueScheduler
118
118
  <Button size="sm" variant={when === "later" ? "secondary" : "ghost"} onClick={() => setWhen("later")}>Schedule</Button>
119
119
  </div>
120
120
  {when === "later" ? <div className="max-w-xs"><Label htmlFor="g8-send-at" className="mb-1">Send at</Label><Input id="g8-send-at" type="datetime-local" value={at} onChange={(e) => setAt(e.target.value)} /></div> : null}
121
- {inPast ? <p role="alert" className="text-sm text-red-700">Pick a time in the future.</p> : null}
121
+ {inPast ? <p role="alert" className="text-sm text-error-foreground">Pick a time in the future.</p> : null}
122
122
  <div><Button disabled={blocked} onClick={() => void openSend()}>{when === "later" ? "Schedule issue" : "Send issue"}</Button></div>
123
123
  {subscribers === 0 && !audience.loading ? <p className="text-muted-foreground text-sm">Add subscribers first (SubscriberTable).</p> : null}
124
124
  </div>
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ /**
4
+ * IssueVersions: the saved versions of one issue, newest first, with restore, and a Duplicate action that copies the
5
+ * issue into a new draft.
6
+ *
7
+ * SDK operations, in the order the screen uses them:
8
+ * 1. listNewsletterIssueVersions GET /newsletters/{id}/issues/{issue}/versions (read)
9
+ * 2. restoreNewsletterIssueVersion POST /newsletters/{id}/issues/{issue}/versions/{n}/restore (write; replaces the draft's content)
10
+ * 3. duplicateIssue POST /newsletters/{id}/issues/{issue}/duplicate (EXTERNAL, confirmed; creates a draft, sends nothing)
11
+ *
12
+ * Only a draft can be restored (a sent issue is a record of what people received). Restoring replaces the draft's
13
+ * current content, so the button says so and asks for a second click before it writes.
14
+ */
15
+ import { useState } from "react";
16
+ import { Copy } from "lucide-react";
17
+ import { Button, Skeleton } from "@graph8/sdk/ui";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
20
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { SectionCard } from "@/g8/components/screen/section-card";
23
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
26
+ import { isDraft } from "../lib/newsletter-types";
27
+ import type { Issue } from "../lib/newsletter-types";
28
+ import { asRec, num, rowsOf, str, versionNumber } from "../lib/newsletter-extra";
29
+ import type { Rec } from "../lib/newsletter-extra";
30
+
31
+ export interface IssueVersionsProps {
32
+ newsletterId: string;
33
+ issue: Pick<Issue, "id" | "status" | "subject">;
34
+ /** Called after a version was restored, so the editor reloads the draft. */
35
+ onRestored?: () => void;
36
+ /** Called with the new draft's id after Duplicate. */
37
+ onDuplicated?: (issueId: number) => void;
38
+ className?: string;
39
+ }
40
+
41
+ const when = (v: unknown): string => {
42
+ const d = new Date(str(v));
43
+ return Number.isNaN(d.getTime()) ? "" : d.toLocaleString();
44
+ };
45
+
46
+ export function IssueVersions({ newsletterId, issue, onRestored, onDuplicated, className }: IssueVersionsProps) {
47
+ const base = `/newsletters/${encodeURIComponent(newsletterId)}/issues/${issue.id}`;
48
+ const { ask, dialog, failure } = useConfirmAction();
49
+ const versions = useG8Query((signal) => g8Get(`${base}/versions`, undefined, { signal }), [base]);
50
+ const [sure, setSure] = useState<number | null>(null);
51
+ const restore = useG8Mutation((n: number) => g8Post(`${base}/versions/${n}/restore`), { onSuccess: () => { setSure(null); void versions.refetch(); onRestored?.(); } });
52
+ const rows = rowsOf<Rec>(versions.data?.data, "versions");
53
+ const draft = isDraft(issue);
54
+
55
+ return (
56
+ <div className={className}>
57
+ <div className="grid gap-4">
58
+ <SectionCard title="Duplicate" description="Copy this issue into a new draft you can change and send separately."
59
+ actions={<Button size="sm" variant="outline" onClick={() => ask({
60
+ tier: "external", title: `Duplicate "${issue.subject || `issue ${issue.id}`}"`, confirmLabel: "Duplicate",
61
+ description: "Creates a new draft with the same content. Nothing is sent to anyone.",
62
+ run: async () => { const id = num(asRec((await g8Post(`${base}/duplicate`)).data).id); if (id !== undefined) onDuplicated?.(id); },
63
+ })}><Copy className="size-4" />Duplicate issue</Button>}>
64
+ <p className="text-muted-foreground text-sm">The copy starts as a draft in this newsletter.</p>
65
+ </SectionCard>
66
+ <SectionCard title="Version history" description={draft ? "Restoring replaces the draft's current content." : "A sent issue cannot be changed; its versions are shown for reference."}>
67
+ {versions.error ? <ApiError error={versions.error} title="Could not load the versions" onRetry={() => void versions.refetch()} /> : versions.loading && !versions.data ? <Skeleton className="h-20 w-full" aria-busy="true" /> : rows.length === 0 ? (
68
+ <EmptyState compact title="No saved versions yet" description="A version is saved each time the issue is saved or rewritten." />
69
+ ) : (
70
+ <ul className="grid gap-2">
71
+ {rows.map((v, i) => {
72
+ const n = versionNumber(v);
73
+ return (
74
+ <li key={n ?? i} className="flex items-center justify-between gap-3 rounded-md border p-3">
75
+ <div className="min-w-0 text-sm"><span className="font-medium">Version {n ?? i + 1}</span>{str(v.source) ? <span className="text-muted-foreground"> from {str(v.source).replace(/_/g, " ")}</span> : null}<span className="text-muted-foreground block text-xs">{when(v.created_at)}{str(v.subject) ? ` · ${str(v.subject)}` : ""}</span></div>
76
+ {draft && n !== undefined ? (
77
+ sure === n ? (
78
+ <div className="flex gap-1"><Button size="sm" loading={restore.loading} onClick={() => void restore.mutate(n)}>Replace the draft</Button><Button size="sm" variant="ghost" onClick={() => setSure(null)}>Cancel</Button></div>
79
+ ) : <Button size="sm" variant="outline" aria-label={`Restore version ${n}`} onClick={() => setSure(n)}>Restore</Button>
80
+ ) : null}
81
+ </li>
82
+ );
83
+ })}
84
+ </ul>
85
+ )}
86
+ {restore.error ? <ApiError error={restore.error} title="Could not restore that version" /> : null}
87
+ </SectionCard>
88
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
89
+ </div>
90
+ <ConfirmSpendDialog {...dialog} />
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ /**
4
+ * MoreRows: the honest end of a list that the API pages. Shows how many rows are on screen and, while the API has
5
+ * another page, a "Load more" button. A list never ends silently: either every row is reachable or this says there are more.
6
+ * (Same contract as the foundation's LoadMore; kept local so the newsletter block does not wait on it.)
7
+ *
8
+ * SDK operations: none.
9
+ */
10
+ import { Button } from "@graph8/sdk/ui";
11
+
12
+ export interface MoreRowsProps {
13
+ /** Rows on screen. */
14
+ shown: number;
15
+ /** What the rows are, plural: "people". */
16
+ noun: string;
17
+ hasMore: boolean;
18
+ loading?: boolean;
19
+ onMore: () => void;
20
+ }
21
+
22
+ export function MoreRows({ shown, noun, hasMore, loading, onMore }: MoreRowsProps) {
23
+ if (!hasMore) return null;
24
+ return (
25
+ <div className="text-muted-foreground flex items-center justify-between gap-3 py-2 text-sm">
26
+ <span role="status">Showing {shown.toLocaleString()} {noun}</span>
27
+ <Button type="button" variant="outline" size="sm" loading={loading} onClick={onMore}>Load more</Button>
28
+ </div>
29
+ );
30
+ }
@@ -81,7 +81,7 @@ export function NewsletterEditor({ initialNewsletterId, onSelectIssue }: Newslet
81
81
  {!current ? <EmptyState title="Pick a newsletter" description="Choose a newsletter to see its issues, or create one." /> : (
82
82
  <>
83
83
  <div className="flex items-center justify-between">
84
- <h2 className="text-lg font-semibold">{current.name} <span className="text-muted-foreground text-sm font-normal">{current.status}</span></h2>
84
+ <h2 className="text-lg font-medium">{current.name} <span className="text-muted-foreground text-sm font-normal">{current.status}</span></h2>
85
85
  {live ? <Button variant="outline" size="sm" loading={pause.loading} onClick={() => void pause.mutate(undefined)}>Pause newsletter</Button> : <Button size="sm" onClick={() => setPending({ kind: "activate" })}>Activate newsletter</Button>}
86
86
  </div>
87
87
  {pause.error ? <ApiError error={pause.error} title="Could not pause" /> : null}
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ /**
4
+ * NewsletterListSyncs: keep subscribers in step with your lists. A synced list adds its contacts as subscribers; run a
5
+ * sync now to pull in the latest, or turn it off.
6
+ *
7
+ * SDK operations, in the order the screen uses them:
8
+ * 1. listNewsletterListSyncs GET /newsletters/{id}/list-syncs (read)
9
+ * 2. listLists GET /lists?page&limit&search (paged, "Load more") (read; the lists you can turn on)
10
+ * 3. enableNewsletterListSync POST /newsletters/{id}/list-syncs { audience_list_id, attested_text } (write, confirmed: people are subscribed to a newsletter)
11
+ * 4. runNewsletterListSync POST /newsletters/{id}/list-syncs/{list}/run (write, confirmed: adds the list's people now)
12
+ * 5. disableNewsletterListSync DELETE /newsletters/{id}/list-syncs/{list} (destructive, confirmed: stops syncing; subscribers stay)
13
+ *
14
+ * Turning a sync on subscribes real people, so the person confirms the consent statement first: the exact text is shown
15
+ * and sent as `attested_text`. The consent claim is theirs; the dialog does not pretend otherwise.
16
+ */
17
+ import { useState } from "react";
18
+ import { Button, Input } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
22
+ import { EmptyState } from "@/g8/components/empty-state";
23
+ import { SectionCard } from "@/g8/components/screen/section-card";
24
+ import { StatusPill } from "@/g8/components/screen/status-pill";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { usePaginated } from "@/g8/hooks/use-paginated";
27
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
28
+ import { asRec, num, pageOf, rowsOf, str } from "../lib/newsletter-extra";
29
+ import type { Rec } from "../lib/newsletter-extra";
30
+ import { MoreRows } from "./more-rows";
31
+
32
+ export interface NewsletterListSyncsProps {
33
+ newsletterId: string;
34
+ /** Called after a sync changed who is subscribed. */
35
+ onChanged?: () => void;
36
+ className?: string;
37
+ }
38
+
39
+ export const CONSENT_TEXT = "I confirm the people in this list agreed to receive this newsletter.";
40
+ const PAGE = 50;
41
+
42
+ export function NewsletterListSyncs({ newsletterId, onChanged, className }: NewsletterListSyncsProps) {
43
+ const base = `/newsletters/${encodeURIComponent(newsletterId)}/list-syncs`;
44
+ const { ask, dialog, failure } = useConfirmAction();
45
+ const [search, setSearch] = useState("");
46
+ const [note, setNote] = useState("");
47
+ const syncs = useG8Query((signal) => g8Get(base, undefined, { signal }), [newsletterId]);
48
+ const rows = rowsOf<Rec>(syncs.data?.data, "list_syncs");
49
+ // Disabled rows stay in the API list (so they can be turned back on): only enabled ones count as synced.
50
+ const synced = new Set(rows.filter((r) => r.enabled !== false).map((r) => String(r.audience_list_id)));
51
+ const lists = usePaginated<Rec>(async (cursor, signal) => {
52
+ const page = cursor ? Number(cursor) : 1;
53
+ const res = await g8Get("/lists", { limit: PAGE, page, search: search.trim() || undefined }, { signal });
54
+ const got = rowsOf<Rec>(res.data, "lists");
55
+ const more = (res.pagination as { has_next?: boolean } | undefined)?.has_next ?? got.length >= PAGE;
56
+ return pageOf(got, more && got.length > 0 ? page + 1 : null);
57
+ }, [search]);
58
+ const changed = (): void => { void syncs.refetch(); onChanged?.(); };
59
+
60
+ const enable = (l: { id?: unknown; title?: unknown; name?: unknown }): void => ask({
61
+ tier: "external", title: `Subscribe "${str(l.title) || str(l.name)}" to this newsletter`, confirmLabel: "Turn on sync",
62
+ description: <><p>People in this list become subscribers and will receive issues. You are confirming:</p><p className="mt-2 font-medium">{CONSENT_TEXT}</p></>,
63
+ run: () => g8Post(base, { audience_list_id: Number(l.id), attested_text: CONSENT_TEXT }), onDone: changed,
64
+ });
65
+ const run = (s: Rec): void => ask({
66
+ tier: "external", title: `Sync "${str(s.name)}" now`, confirmLabel: "Sync now",
67
+ recipients: num(s.contact_count),
68
+ description: "Adds the list's people who are not subscribed yet. People who unsubscribed stay unsubscribed.",
69
+ run: async () => { await g8Post(`${base}/${encodeURIComponent(String(s.audience_list_id))}/run`); setNote(`Synced "${str(s.name)}".`); }, onDone: changed,
70
+ });
71
+ const disable = (s: Rec): void => ask({
72
+ tier: "destructive", title: `Stop syncing "${str(s.name)}"`, confirmLabel: "Stop syncing",
73
+ description: "New contacts added to this list are no longer subscribed. People already subscribed stay subscribed.",
74
+ tolerateStatus: [404], run: () => g8Delete(`${base}/${encodeURIComponent(String(s.audience_list_id))}`), onDone: changed,
75
+ });
76
+
77
+ return (
78
+ <div className={className}>
79
+ <div className="grid gap-4">
80
+ <SectionCard title="Synced lists" description="Subscribers are kept in step with these lists.">
81
+ {syncs.error ? <ApiError error={syncs.error} title="Could not load the synced lists" onRetry={() => void syncs.refetch()} /> : rows.length === 0 ? (
82
+ <EmptyState compact title="No lists are synced" description="Choose a list below to subscribe its people." />
83
+ ) : (
84
+ <ul className="grid gap-2">
85
+ {rows.map((s) => (
86
+ <li key={String(s.audience_list_id)} className="flex items-center justify-between gap-3 rounded-md border p-3">
87
+ <div className="min-w-0"><span className="block truncate text-sm font-medium">{str(s.name) || `List ${String(s.audience_list_id)}`}</span><span className="text-muted-foreground text-xs">{num(s.contact_count) == null ? "" : `${(s.contact_count as number).toLocaleString()} contacts`}</span></div>
88
+ {s.enabled === false ? <StatusPill tone="neutral">Off</StatusPill> : <StatusPill tone="positive">On</StatusPill>}
89
+ {s.enabled === false ? (
90
+ <Button size="sm" variant="outline" aria-label={`Turn ${str(s.name)} back on`} onClick={() => enable({ id: s.audience_list_id, name: s.name })}>Turn on sync</Button>
91
+ ) : <div className="flex gap-1"><Button size="sm" variant="outline" aria-label={`Sync ${str(s.name)} now`} onClick={() => run(s)}>Sync now</Button><Button size="sm" variant="ghost" aria-label={`Stop syncing ${str(s.name)}`} onClick={() => disable(s)}>Stop</Button></div>}
92
+ </li>
93
+ ))}
94
+ </ul>
95
+ )}
96
+ {note ? <p role="status" className="mt-2 text-sm">{note}</p> : null}
97
+ </SectionCard>
98
+ <SectionCard title="Add a list" description="Find the list to subscribe.">
99
+ <Input aria-label="Search lists" placeholder="Search lists" value={search} onChange={(e) => setSearch(e.target.value)} className="mb-3 max-w-sm" />
100
+ {lists.error ? <ApiError error={lists.error} title="Could not load your lists" onRetry={() => void lists.reload()} /> : lists.loading ? <p className="text-muted-foreground text-sm" aria-busy="true">Loading lists...</p> : lists.items.length === 0 ? (
101
+ <EmptyState compact title={search ? "No list matches" : "You have no lists"} description={search ? "Try another name." : "Create a list in Lists first."} />
102
+ ) : (
103
+ <>
104
+ <ul className="grid gap-2">
105
+ {lists.items.map((l) => (
106
+ <li key={String(l.id)} className="flex items-center justify-between gap-3 rounded-md border p-3">
107
+ <span className="truncate text-sm">{str(l.title)}</span>
108
+ {synced.has(String(l.id)) ? <StatusPill tone="positive">Synced</StatusPill> : <Button size="sm" variant="outline" aria-label={`Sync ${str(l.title)}`} onClick={() => enable(l)}>Turn on sync</Button>}
109
+ </li>
110
+ ))}
111
+ </ul>
112
+ <MoreRows shown={lists.items.length} noun="lists" hasMore={lists.hasMore} loading={lists.loadingMore} onMore={() => void lists.loadMore()} />
113
+ </>
114
+ )}
115
+ </SectionCard>
116
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
117
+ </div>
118
+ <ConfirmSpendDialog {...dialog} />
119
+ </div>
120
+ );
121
+ }
@@ -5,10 +5,13 @@
5
5
  *
6
6
  * Composes: ScreenLayout (title, tabs, stats), Toolbar, SplitView + DetailPanel (selected issue, deep link
7
7
  * `?id=`), FormSheet (new newsletter), IssueForm (write, AI, test, delete), IssueScheduler (send, schedule,
8
- * pause, resume, cancel), SubscriberTable (add, import a list, verify, unsubscribe, erase), NewsletterStats.
8
+ * pause, resume, cancel), SubscriberTable (add, import a list, verify, unsubscribe, erase), NewsletterStats,
9
+ * NewsletterSettings (name, sender, opt-in, sending domain, running state), NewsletterListSyncs, NewsletterTemplates,
10
+ * IssueVersions (history, restore, duplicate) and SubscriberTimeline.
9
11
  *
10
12
  * Every record has a full detail view: `?tab=` picks Issues, Subscribers, Performance or Details (the newsletter itself);
11
- * `?id=` opens an issue (Write, Send, Results, Details) or a subscriber (all fields, unsubscribe, erase);
13
+ * `?id=` opens an issue (Write, Send, Results, History, Details) or a subscriber (all fields, activity, unsubscribe, erase);
14
+ * the Settings, Lists and Templates tabs manage the newsletter itself, its synced lists and its saved templates;
12
15
  * `&view=full` shows the open record as a full page.
13
16
  *
14
17
  * User flows: pick or create a newsletter (`?newsletter=`), activate or pause it, read the headline numbers,
@@ -39,12 +42,18 @@ import { Toolbar } from "@/g8/components/screen/toolbar";
39
42
  import { useUrlState } from "@/g8/hooks/use-url-state";
40
43
  import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
41
44
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
45
+ import { useAllPages } from "@/g8/hooks/use-all-pages";
42
46
  import { useG8Query } from "@/g8/hooks/use-g8-query";
43
47
  import { isDraft, issueTitle, percent } from "../lib/newsletter-types";
44
48
  import type { Issue, IssueKpis, Newsletter, Subscriber } from "../lib/newsletter-types";
45
49
  import { IssueForm } from "./issue-form";
46
50
  import { IssueScheduler } from "./issue-scheduler";
51
+ import { IssueVersions } from "./issue-versions";
52
+ import { NewsletterListSyncs } from "./newsletter-list-syncs";
53
+ import { NewsletterSettings } from "./newsletter-settings";
47
54
  import { NewsletterStats } from "./newsletter-stats";
55
+ import { NewsletterTemplates } from "./newsletter-templates";
56
+ import { SubscriberTimeline } from "./subscriber-timeline";
48
57
  import { SubscriberTable } from "./subscriber-table";
49
58
  import { useBrand } from "@/g8/hooks/use-brand";
50
59
 
@@ -58,7 +67,9 @@ export interface NewsletterScreenProps {
58
67
  className?: string;
59
68
  }
60
69
 
61
- type Tab = "issues" | "subscribers" | "performance" | "details";
70
+ type Tab = "issues" | "subscribers" | "performance" | "details" | "settings" | "lists" | "templates";
71
+ /** Subscribers per read when looking one up for a deep link; every page is read. */
72
+ const SUBSCRIBER_PAGE = 100; // the API caps page_size at 100 (a larger value is a 422)
62
73
  const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
63
74
 
64
75
  function tone(status: string | undefined): Tone {
@@ -119,7 +130,7 @@ function IssueDetails({ issue }: { issue: Issue }) {
119
130
  );
120
131
  }
121
132
 
122
- function SubscriberDetail({ newsletterId, subscriber, onChanged, onClose, expandProps }: { newsletterId: string; subscriber: Subscriber; onChanged: () => void; onClose: () => void; expandProps: { onExpand: () => void; onCollapse?: () => void } }) {
133
+ function SubscriberDetail({ newsletterId, subscriber, issues, onChanged, onClose, expandProps }: { newsletterId: string; subscriber: Subscriber; issues?: Issue[]; onChanged: () => void; onClose: () => void; expandProps: { onExpand: () => void; onCollapse?: () => void } }) {
123
134
  const [confirm, setConfirm] = useState<"unsubscribe" | "erase" | null>(null);
124
135
  const base = `/newsletters/${encodeURIComponent(newsletterId)}/subscribers/${subscriber.id}`;
125
136
  const run = useG8Mutation((kind: "unsubscribe" | "erase") => (kind === "unsubscribe" ? g8Patch(`${base}/unsubscribe`, {}) : g8Delete(base)), {
@@ -155,6 +166,7 @@ function SubscriberDetail({ newsletterId, subscriber, onChanged, onClose, expand
155
166
  ...extra.map(([key, v]) => ({ label: key.replace(/_/g, " "), value: String(v) })),
156
167
  ]}
157
168
  />
169
+ {issues ? <SubscriberTimeline className="mt-4" newsletterId={newsletterId} subscriberId={subscriber.id} issues={issues} /> : null}
158
170
  <ConfirmSpendDialog
159
171
  open={confirm !== null}
160
172
  tier={confirm === "erase" ? "destructive" : "external"}
@@ -177,7 +189,7 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
177
189
  const [view, setView] = useUrlState("view", "");
178
190
  const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
179
191
  const [tabParam, setTabParam] = useUrlState("tab", "issues");
180
- const tab = (["issues", "subscribers", "performance", "details"].includes(tabParam) ? tabParam : "issues") as Tab;
192
+ const tab = (["issues", "subscribers", "performance", "details", "settings", "lists", "templates"].includes(tabParam) ? tabParam : "issues") as Tab;
181
193
  const closeDetail = (): void => { setIssueParam(null); setView(null); };
182
194
  const [subRefresh, setSubRefresh] = useState(0);
183
195
  const [openedSubscriber, setOpenedSubscriber] = useState<Subscriber | null>(null);
@@ -187,6 +199,8 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
187
199
  const [name, setName] = useState("");
188
200
  const [goal, setGoal] = useState("");
189
201
  const [pending, setPending] = useState<"create" | "activate" | null>(null);
202
+ // Bumped after a version restore so the editor reloads the draft instead of showing the old text.
203
+ const [formRev, setFormRev] = useState(0);
190
204
 
191
205
  const newsletters = useG8Query((signal) => g8Get<Newsletter[]>("/newsletters", {}, { signal }).then((r) => r.data ?? []), []);
192
206
  const list = newsletters.data ?? [];
@@ -209,12 +223,17 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
209
223
  const pause = useG8Mutation(() => g8Post(`${base}/pause`), { onSuccess: () => void newsletters.refetch() });
210
224
 
211
225
  const subscriberId = tab === "subscribers" && issueParam ? Number(issueParam) : undefined;
212
- const lookup = useG8Query(
213
- (signal) => g8Get<{ items?: Subscriber[] }>(`${base}/subscribers`, { page: 1, page_size: 200 }, { signal }).then((r) => r.data?.items ?? []),
226
+ // A deep link to a subscriber reads every page (100 per read), so no subscriber is out of reach.
227
+ const lookup = useAllPages<Subscriber>(
228
+ async (page, signal) => {
229
+ const r = await g8Get<{ items?: Subscriber[]; total?: number }>(`${base}/subscribers`, { page, page_size: SUBSCRIBER_PAGE }, { signal });
230
+ const rows = r.data?.items ?? [];
231
+ return { rows, hasNext: r.data?.total != null ? page * SUBSCRIBER_PAGE < r.data.total : rows.length >= SUBSCRIBER_PAGE };
232
+ },
214
233
  [base, subRefresh],
215
- { enabled: subscriberId !== undefined && openedSubscriber?.id !== subscriberId },
234
+ { enabled: Boolean(newsletterId) && subscriberId !== undefined && openedSubscriber?.id !== subscriberId },
216
235
  );
217
- const openSubscriber = subscriberId === undefined ? null : openedSubscriber?.id === subscriberId ? openedSubscriber : ((lookup.data ?? []).find((x) => x.id === subscriberId) ?? null);
236
+ const openSubscriber = subscriberId === undefined ? null : openedSubscriber?.id === subscriberId ? openedSubscriber : (lookup.items.find((x) => x.id === subscriberId) ?? null);
218
237
  const all = issues.data ?? [];
219
238
  const sent = all.filter((i) => i.status === "sent" || i.sent_at);
220
239
  const totals = sent.reduce((a, i) => ({ delivered: a.delivered + (i.kpis?.delivered ?? 0), opened: a.opened + (i.kpis?.opened ?? 0), clicked: a.clicked + (i.kpis?.clicked ?? 0) }), { delivered: 0, opened: 0, clicked: 0 });
@@ -227,9 +246,10 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
227
246
  const pickNewsletter = (id: string): void => { setIssueParam(null); setUrlNewsletter(id); };
228
247
  const detailTabs = selected
229
248
  ? [
230
- { id: "write", label: isDraft(selected) ? "Write" : "Content", content: <IssueForm key={`f${selected.id}`} newsletterId={newsletterId} issueId={selected.id} onSaved={() => void issues.refetch()} onDeleted={() => { setIssueParam(null); void issues.refetch(); }} /> },
249
+ { id: "write", label: isDraft(selected) ? "Write" : "Content", content: <IssueForm key={`f${selected.id}:${formRev}`} newsletterId={newsletterId} issueId={selected.id} onSaved={() => void issues.refetch()} onDeleted={() => { setIssueParam(null); void issues.refetch(); }} /> },
231
250
  { id: "send", label: "Send", content: <IssueScheduler key={`s${selected.id}`} newsletterId={newsletterId} issueId={selected.id} onSent={() => void issues.refetch()} /> },
232
251
  { id: "results", label: "Results", content: <IssueResults newsletterId={newsletterId} issue={selected} /> },
252
+ { id: "history", label: "History", content: <IssueVersions key={`v${selected.id}:${formRev}`} newsletterId={newsletterId} issue={selected} onRestored={() => { setFormRev((n) => n + 1); void issues.refetch(); }} onDuplicated={(id) => { void issues.refetch(); setIssueParam(String(id)); }} /> },
233
253
  { id: "details", label: "Issue details", content: <IssueDetails issue={selected} /> },
234
254
  ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
235
255
  ]
@@ -287,9 +307,9 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
287
307
  detailWidth="lg"
288
308
  list={<SubscriberTable key={newsletterId} newsletterId={newsletterId} refreshKey={subRefresh} selectedId={subscriberId} onOpen={(sub) => { setOpenedSubscriber(sub); setIssueParam(String(sub.id)); }} />}
289
309
  detail={openSubscriber ? (
290
- <SubscriberDetail key={openSubscriber.id} newsletterId={newsletterId} subscriber={openSubscriber} onChanged={() => { setSubRefresh((n) => n + 1); setOpenedSubscriber(null); }} onClose={() => { closeDetail(); setOpenedSubscriber(null); }} expandProps={expandProps} />
310
+ <SubscriberDetail key={openSubscriber.id} newsletterId={newsletterId} subscriber={openSubscriber} issues={all} onChanged={() => { setSubRefresh((n) => n + 1); setOpenedSubscriber(null); }} onClose={() => { closeDetail(); setOpenedSubscriber(null); }} expandProps={expandProps} />
291
311
  ) : lookup.loading ? <Skeleton className="h-40 w-full" aria-busy="true" /> : (
292
- <EmptyState title="Subscriber not found" description="Open a subscriber from the list. Direct links reach the first 200 subscribers." />
312
+ <EmptyState title="Subscriber not found" description="Open a subscriber from the list." />
293
313
  )}
294
314
  />
295
315
  )
@@ -320,6 +340,9 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
320
340
  </DetailPanel>
321
341
  )
322
342
  : tab === "performance" ? <NewsletterStats key={newsletterId} newsletterId={newsletterId} />
343
+ : tab === "settings" ? <NewsletterSettings key={newsletterId} newsletterId={newsletterId} onChanged={() => void newsletters.refetch()} />
344
+ : tab === "lists" ? <NewsletterListSyncs key={newsletterId} newsletterId={newsletterId} onChanged={() => setSubRefresh((n) => n + 1)} />
345
+ : tab === "templates" ? <NewsletterTemplates key={newsletterId} newsletterId={newsletterId} onUsed={(id) => { void issues.refetch(); setTabParam(null); setIssueParam(String(id)); }} />
323
346
  : (
324
347
  <SplitView
325
348
  detailOpen={detailOpen}
@@ -364,7 +387,7 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
364
387
  <Button size="sm" onClick={() => setSheet(true)}><Plus className="size-4" />New newsletter</Button>
365
388
  </>
366
389
  )}
367
- tabs={current ? [{ id: "issues", label: "Issues", count: all.length }, { id: "subscribers", label: "Subscribers", count: audience.data }, { id: "performance", label: "Performance" }, { id: "details", label: "Details" }] : undefined}
390
+ tabs={current ? [{ id: "issues", label: "Issues", count: all.length }, { id: "subscribers", label: "Subscribers", count: audience.data }, { id: "performance", label: "Performance" }, { id: "templates", label: "Templates" }, { id: "lists", label: "Lists" }, { id: "settings", label: "Settings" }, { id: "details", label: "Details" }] : undefined}
368
391
  activeTab={tab}
369
392
  onTabChange={(id) => { setTabParam(id === "issues" ? null : id); closeDetail(); setOpenedSubscriber(null); }}
370
393
  stats={current ? [