@graph8/sdk 0.370.0 → 0.374.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 (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -15,13 +15,23 @@
15
15
  * renders its own ScreenLayout header ("Inbox" h1, stats). Opt out with `urlState={false}` (selection kept in component
16
16
  * state, the URL untouched) and/or `hideHeader` (no title or stats, just the toolbar and the two panes). Defaults keep the new look.
17
17
  *
18
+ * Management (all optional, on by default; `showManagement={false}` hides the three toolbar buttons):
19
+ * - "New message" (ComposeMessage): write a new email or text from a sender you choose (external, confirmed).
20
+ * - "Search all" (InboxSearch): search every email thread or LinkedIn conversation on the server, then open a result.
21
+ * - "Manage" (TagManager + BlockedSenders): create, rename, delete tags; block and unblock senders. The tag list it
22
+ * loads also feeds the tag filter and the tag picker.
23
+ * - In an open conversation, with `showThreadActions` (default off), "Manage conversation" (ThreadActions): mark read or unread, remove a tag, unassign, block sender.
24
+ * SDK operations of its own: none; the parts above own theirs and their confirm dialogs.
25
+ * A conversation this key cannot see (or that was removed) answers 404; the conversation view says so instead of failing.
26
+ *
18
27
  * Props are backwards compatible: `initialFilters`, `availableTags`, `className` as before; everything else is optional glue.
19
28
  */
20
29
  import { useMemo, useState, type ReactNode } from "react";
21
- import { Inbox as InboxIcon, Mail, RefreshCw } from "lucide-react";
30
+ import { Inbox as InboxIcon, Mail, PenSquare, RefreshCw, Search, Tags } from "lucide-react";
22
31
  import { Button, Skeleton } from "@graph8/sdk/ui";
23
32
  import { ApiError } from "@/g8/components/api-error";
24
33
  import { EmptyState } from "@/g8/components/empty-state";
34
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
25
35
  import { DetailPanel } from "@/g8/components/screen/detail-panel";
26
36
  import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
27
37
  import { KeyValueList } from "@/g8/components/screen/key-value-list";
@@ -42,7 +52,13 @@ import {
42
52
  type InboxTag,
43
53
  type InboxThread,
44
54
  } from "../lib/inbox-types";
55
+ import { isNotFound } from "../lib/inbox-manage";
56
+ import { BlockedSenders } from "./blocked-senders";
57
+ import { ComposeMessage } from "./compose-message";
58
+ import { InboxSearch } from "./inbox-search";
45
59
  import { ReplyComposer } from "./reply-composer";
60
+ import { TagManager } from "./tag-manager";
61
+ import { ThreadActions } from "./thread-actions";
46
62
  import { TagAndAssign } from "./tag-and-assign";
47
63
  import { ThreadView } from "./thread-view";
48
64
  import { useBrand } from "@/g8/hooks/use-brand";
@@ -66,6 +82,10 @@ export interface InboxScreenProps {
66
82
  urlState?: boolean;
67
83
  /** Skip the screen header (title, description, stats) when the host page already has one. Default false. */
68
84
  hideHeader?: boolean;
85
+ /** Show the New message, Search all and Manage buttons. Default true. */
86
+ showManagement?: boolean;
87
+ /** Show "Manage conversation" (mark read, remove tag, unassign, block sender) under the composer. Default false, so existing embedders see no new controls. */
88
+ showThreadActions?: boolean;
69
89
  }
70
90
 
71
91
  /** `useUrlState` or plain state, chosen once per mount (the flag must not change while mounted). */
@@ -160,6 +180,7 @@ function ListEmpty({ filtered, onClear }: { filtered: boolean; onClear: () => vo
160
180
 
161
181
  function ThreadDetails({ thread }: { thread: Selected }) {
162
182
  const { data: t, loading, error, refetch } = useInboxThread(thread.id, thread.channel);
183
+ if (isNotFound(error)) return <EmptyState compact title="Conversation not found" description="It may have been removed, or it is not shared with this API key." />;
163
184
  if (error) return <ApiError error={error} title="Could not load this conversation" onRetry={() => void refetch()} />;
164
185
  if (!t) return loading ? <Skeleton className="h-24 w-full" /> : null;
165
186
  const dash = "";
@@ -185,15 +206,18 @@ function ThreadDetails({ thread }: { thread: Selected }) {
185
206
 
186
207
  function ExtraTab({ thread, render }: { thread: Selected; render: (thread: InboxThread) => ReactNode }) {
187
208
  const { data, loading, error, refetch } = useInboxThread(thread.id, thread.channel);
209
+ if (isNotFound(error)) return <EmptyState compact title="Conversation not found" description="It may have been removed, or it is not shared with this API key." />;
188
210
  if (error) return <ApiError error={error} title="Could not load this conversation" onRetry={() => void refetch()} />;
189
211
  if (!data) return loading ? <Skeleton className="h-24 w-full" /> : null;
190
212
  return <>{render(data)}</>;
191
213
  }
192
214
 
193
- export function InboxScreen({ initialFilters, availableTags, className, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, pageSize = 25, urlState = true, hideHeader = false }: InboxScreenProps) {
215
+ export function InboxScreen({ initialFilters, availableTags, className, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, pageSize = 25, urlState = true, hideHeader = false, showManagement = true, showThreadActions = false }: InboxScreenProps) {
194
216
  const [filters, setFilters] = useState<InboxFilters>(initialFilters ?? {});
195
217
  const [page, setPage] = useState(1);
196
218
  const [search, setSearch] = useState("");
219
+ const [sheet, setSheet] = useState<"compose" | "search" | "manage" | null>(null);
220
+ const [managed, setManaged] = useState<InboxTag[]>([]);
197
221
  const [openKey, setOpenKey] = useSelectionState("id", urlState);
198
222
  const [view, setView] = useSelectionState("view", urlState);
199
223
  const { threads, loading, error, hasNext, total, refetch } = useInboxThreads(filters, page, pageSize);
@@ -202,9 +226,9 @@ export function InboxScreen({ initialFilters, availableTags, className, toolbarA
202
226
 
203
227
  const tags = useMemo(() => {
204
228
  const seen = new Map<string, InboxTag>();
205
- for (const t of [...(availableTags ?? []), ...threads.flatMap((th) => th.tags)]) if (t.id) seen.set(t.id, t);
229
+ for (const t of [...(availableTags ?? []), ...managed, ...threads.flatMap((th) => th.tags)]) if (t.id) seen.set(t.id, t);
206
230
  return [...seen.values()];
207
- }, [availableTags, threads]);
231
+ }, [availableTags, managed, threads]);
208
232
  const needle = search.trim().toLowerCase();
209
233
  const rows = useMemo(
210
234
  () => (needle ? threads.filter((t) => `${contactLabel(t)} ${t.contact?.email ?? ""} ${t.contact?.company ?? ""} ${t.subject ?? ""} ${previewText(t, 300)}`.toLowerCase().includes(needle)) : threads),
@@ -222,6 +246,27 @@ export function InboxScreen({ initialFilters, availableTags, className, toolbarA
222
246
  const count = (c: string) => threads.filter((t) => t.channel === c).length;
223
247
  const first = open ? threads.find((t) => t.id === open.id && t.channel === open.channel) : undefined;
224
248
 
249
+ const management = showManagement ? (
250
+ <>
251
+ <Button size="sm" variant="outline" onClick={() => setSheet("search")}><Search className="mr-1 size-4" />Search all</Button>
252
+ <Button size="sm" variant="outline" onClick={() => setSheet("manage")}><Tags className="mr-1 size-4" />Manage</Button>
253
+ <Button size="sm" onClick={() => setSheet("compose")}><PenSquare className="mr-1 size-4" />New message</Button>
254
+ </>
255
+ ) : null;
256
+ const sheets = showManagement ? (
257
+ <>
258
+ <FormSheet open={sheet === "compose"} onOpenChange={(o) => !o && setSheet(null)} width="lg" title="New message" description="Write to someone new. Nothing is sent until you confirm.">
259
+ {sheet === "compose" ? <ComposeMessage onCancel={() => setSheet(null)} onSent={() => { setSheet(null); void refetch(); }} /> : null}
260
+ </FormSheet>
261
+ <FormSheet open={sheet === "search"} onOpenChange={(o) => !o && setSheet(null)} width="lg" title="Search all conversations" description="Searches your email and LinkedIn on the server, beyond the page loaded here.">
262
+ {sheet === "search" ? <InboxSearch onOpen={(hit) => { setOpenKey(`${hit.channel}:${hit.id}`); setView(null); setSheet(null); }} /> : null}
263
+ </FormSheet>
264
+ <FormSheet open={sheet === "manage"} onOpenChange={(o) => !o && setSheet(null)} width="lg" title="Manage the inbox" description="Tags and blocked senders.">
265
+ {sheet === "manage" ? <div className="space-y-6"><TagManager onTags={setManaged} /><BlockedSenders /></div> : null}
266
+ </FormSheet>
267
+ </>
268
+ ) : null;
269
+
225
270
  const panes = (
226
271
  <>
227
272
  <div className={expanded ? "grid gap-4" : "grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]"}>
@@ -268,6 +313,7 @@ export function InboxScreen({ initialFilters, availableTags, className, toolbarA
268
313
  {renderDetailActions ? <div className="flex flex-wrap gap-2">{renderDetailActions(thread)}</div> : null}
269
314
  <ReplyComposer thread={thread} onSent={reload} />
270
315
  <TagAndAssign thread={thread} availableTags={tags} onChanged={reload} />
316
+ {showThreadActions ? <ThreadActions thread={thread} onChanged={reload} /> : null}
271
317
  </div>
272
318
  )}
273
319
  </ThreadView>
@@ -289,9 +335,10 @@ export function InboxScreen({ initialFilters, availableTags, className, toolbarA
289
335
  <div className={className ? `space-y-4 ${className}` : "space-y-4"}>
290
336
  <div className="flex flex-wrap items-center justify-between gap-2">
291
337
  <Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search this page" }} filters={<Filters filters={filters} tags={tags} update={update} />} />
292
- <div className="flex gap-2"><Button size="sm" variant="outline" onClick={() => void refetch()}><RefreshCw className="mr-1 size-4" />Refresh</Button>{toolbarActions}</div>
338
+ <div className="flex gap-2"><Button size="sm" variant="outline" onClick={() => void refetch()}><RefreshCw className="mr-1 size-4" />Refresh</Button>{management}{toolbarActions}</div>
293
339
  </div>
294
340
  {panes}
341
+ {sheets}
295
342
  </div>
296
343
  );
297
344
  }
@@ -301,7 +348,7 @@ export function InboxScreen({ initialFilters, availableTags, className, toolbarA
301
348
  title="Inbox"
302
349
  description="Every reply across email, SMS and LinkedIn in one queue."
303
350
  icon={<InboxIcon className="size-5" />}
304
- actions={<><Button size="sm" variant="outline" onClick={() => void refetch()}><RefreshCw className="mr-1 size-4" />Refresh</Button>{toolbarActions}</>}
351
+ actions={<><Button size="sm" variant="outline" onClick={() => void refetch()}><RefreshCw className="mr-1 size-4" />Refresh</Button>{management}{toolbarActions}</>}
305
352
  stats={[
306
353
  { label: "Conversations", value: total != null ? total.toLocaleString() : threads.length, hint: total != null ? "in this view" : "on this page" },
307
354
  { label: "Drafts waiting", value: drafts, hint: "on this page", tone: drafts > 0 ? "warning" : "neutral" },
@@ -311,6 +358,7 @@ export function InboxScreen({ initialFilters, availableTags, className, toolbarA
311
358
  toolbar={<Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search this page" }} filters={<Filters filters={filters} tags={tags} update={update} />} />}
312
359
  >
313
360
  {panes}
361
+ {sheets}
314
362
  </ScreenLayout>
315
363
  );
316
364
  }
@@ -0,0 +1,106 @@
1
+ "use client";
2
+
3
+ /**
4
+ * InboxSearch: search ALL of your email threads or LinkedIn conversations on the server, not just the page the list
5
+ * has loaded. Type, press Enter (or Search), pick a result; `onOpen` receives the thread's channel and id so a screen
6
+ * can open it in its conversation view.
7
+ *
8
+ * SDK operations, in order:
9
+ * Email: 1. GET /inbox/mailboxes/paginated (aiInbox.listInboxMailboxesPaginated) READ - the mailboxes searched
10
+ * 2. POST /inbox/emails/search?page&page_size (inbox.searchInboxEmails) READ - `mailboxes` is required
11
+ * (an empty mailbox list is "no result", never "all", so every mailbox you can see is sent)
12
+ * LinkedIn: POST /inbox/linkedin/conversations/search (aiInbox.searchInboxLinkedinConversations) READ
13
+ * SMS threads have no search route in the API; they are found in the list. Searching sends nothing and costs nothing.
14
+ * Result rows are untyped in the contract, so they are read defensively; opening one loads the real thread
15
+ * (ThreadView), and a thread the key may not see answers a uniform 404 which the thread view explains.
16
+ */
17
+ import { useState } from "react";
18
+ import { Search } from "lucide-react";
19
+ import { Button, Input, Skeleton } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { g8Post } from "@/g8/lib/g8-client";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { FirstNotice } from "@/g8/components/paging-notices";
25
+ import { useInboxMailboxes } from "../hooks/use-inbox-mailboxes";
26
+ import { formatWhen } from "../lib/inbox-types";
27
+ import { rowsFrom, searchHit } from "../lib/inbox-manage";
28
+ import type { SearchHit } from "../lib/inbox-manage";
29
+
30
+ export interface InboxSearchProps {
31
+ /** Called with the chosen result's channel and thread id. */
32
+ onOpen?: (hit: { channel: "email" | "linkedin"; id: string }) => void;
33
+ pageSize?: number;
34
+ className?: string;
35
+ }
36
+
37
+ type Channel = "email" | "linkedin";
38
+
39
+ export function InboxSearch({ onOpen, pageSize = 20, className }: InboxSearchProps) {
40
+ const [channel, setChannel] = useState<Channel>("email");
41
+ const [text, setText] = useState("");
42
+ const [submitted, setSubmitted] = useState<{ channel: Channel; q: string; page: number } | null>(null);
43
+ const mailboxes = useInboxMailboxes({ enabled: channel === "email" });
44
+ const results = useG8Query(
45
+ async (signal): Promise<{ hits: SearchHit[]; hasNext: boolean }> => {
46
+ const s = submitted!;
47
+ if (s.channel === "email") {
48
+ const env = await g8Post("/inbox/emails/search", { mailboxes: (mailboxes.data ?? []).map((m) => m.address), search: s.q, sort_by: "updated_at", sort_order: "desc" }, { query: { page: s.page, page_size: pageSize }, signal });
49
+ const hits = rowsFrom<Record<string, unknown>>(env, "emails").map((r) => searchHit("email", r)).filter((h): h is SearchHit => h != null);
50
+ return { hits, hasNext: Boolean((env.pagination as { has_next?: boolean } | undefined)?.has_next) || hits.length >= pageSize };
51
+ }
52
+ const env = await g8Post("/inbox/linkedin/conversations/search", { search: s.q, limit: pageSize, offset: (s.page - 1) * pageSize, sort_order: "desc" }, { signal });
53
+ const hits = rowsFrom<Record<string, unknown>>(env, "conversations").map((r) => searchHit("linkedin", r)).filter((h): h is SearchHit => h != null);
54
+ return { hits, hasNext: hits.length >= pageSize };
55
+ },
56
+ [submitted?.channel, submitted?.q, submitted?.page, submitted ? (mailboxes.data ?? []).length : 0],
57
+ { enabled: submitted != null && (submitted.channel !== "email" || mailboxes.data != null) },
58
+ );
59
+
60
+ const run = () => {
61
+ const q = text.trim();
62
+ if (q) setSubmitted({ channel, q, page: 1 });
63
+ };
64
+ const hits = results.data?.hits ?? [];
65
+
66
+ return (
67
+ <div className={`space-y-3 ${className ?? ""}`}>
68
+ <form className="flex flex-wrap items-center gap-2" role="search" onSubmit={(e) => { e.preventDefault(); run(); }}>
69
+ <div role="group" aria-label="Search in" className="flex gap-1">
70
+ <Button type="button" size="sm" variant={channel === "email" ? "default" : "outline"} onClick={() => { setChannel("email"); setSubmitted(null); }}>Email</Button>
71
+ <Button type="button" size="sm" variant={channel === "linkedin" ? "default" : "outline"} onClick={() => { setChannel("linkedin"); setSubmitted(null); }}>LinkedIn</Button>
72
+ </div>
73
+ <Input aria-label="Search all conversations" className="min-w-48 flex-1" placeholder={channel === "email" ? "Subject, sender or text" : "Name or message text"} value={text} onChange={(e) => setText(e.target.value)} />
74
+ <Button type="submit" size="sm" disabled={!text.trim()}><Search className="mr-1 size-4" aria-hidden />Search</Button>
75
+ </form>
76
+ {channel === "email" ? <FirstNotice shown={mailboxes.items.length} limit={0} force={mailboxes.truncated} noun="mailboxes" /> : null}
77
+ {mailboxes.error && channel === "email" ? <ApiError error={mailboxes.error} title="Could not load your mailboxes" onRetry={() => void mailboxes.refetch()} /> : null}
78
+ {channel === "email" && mailboxes.data && mailboxes.data.length === 0 ? <p className="text-sm text-muted-foreground">No mailbox is connected yet, so there is nothing to search.</p> : null}
79
+ {results.error ? <ApiError error={results.error} title="The search failed" onRetry={() => void results.refetch()} /> : null}
80
+ {submitted && results.loading && !results.data ? <div aria-busy="true" className="space-y-2"><Skeleton className="h-14 w-full" /><Skeleton className="h-14 w-full" /></div> : null}
81
+ {submitted && results.data && hits.length === 0 ? <EmptyState compact title="No conversations found" description={`Nothing matches "${submitted.q}".`} /> : null}
82
+ {hits.length > 0 ? (
83
+ <ul aria-label="Search results" className="divide-y rounded-md border border-border">
84
+ {hits.map((h) => (
85
+ <li key={`${h.channel}:${h.id}`}>
86
+ <button type="button" className="flex w-full flex-col gap-0.5 p-3 text-left hover:bg-muted/50" onClick={() => onOpen?.({ channel: h.channel, id: h.id })}>
87
+ <span className="flex items-center justify-between gap-2"><span className="truncate text-sm font-medium">{h.title}</span><span className="shrink-0 text-xs text-muted-foreground">{formatWhen(h.when)}</span></span>
88
+ {h.subtitle ? <span className="truncate text-xs text-muted-foreground">{h.subtitle}</span> : null}
89
+ {h.preview ? <span className="truncate text-xs">{h.preview}</span> : null}
90
+ </button>
91
+ </li>
92
+ ))}
93
+ </ul>
94
+ ) : null}
95
+ {submitted && results.data && (submitted.page > 1 || results.data.hasNext) ? (
96
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
97
+ <span>Page {submitted.page}</span>
98
+ <div className="flex gap-2">
99
+ <Button size="sm" variant="outline" disabled={submitted.page <= 1 || results.loading} onClick={() => setSubmitted({ ...submitted, page: submitted.page - 1 })}>Previous</Button>
100
+ <Button size="sm" variant="outline" disabled={!results.data.hasNext || results.loading} onClick={() => setSubmitted({ ...submitted, page: submitted.page + 1 })}>Next</Button>
101
+ </div>
102
+ </div>
103
+ ) : null}
104
+ </div>
105
+ );
106
+ }
@@ -0,0 +1,154 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordConversationsTab: the email conversations with one contact, newest first, "Load more" for older ones. Opening a row
5
+ * shows its messages. Use it as an `extraDetailTabs` entry of ContactsScreen:
6
+ * { id: "conversations", label: "Conversations", render: (c) => <RecordConversationsTab email={c.work_email} onOpenThread={(sel) => router.push(`/inbox?id=${sel}`)} /> }
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listInboxMailboxes GET /inbox/mailboxes (read; the mailboxes this key can read: search requires them)
10
+ * 2. searchInboxEmails POST /inbox/emails/search?page=&page_size= (read; body `{ mailboxes, search_emails: [email] }`, 10 per page)
11
+ * 3. getReply GET /inbox/{id}?channel=email (read; only when a row is opened)
12
+ * The inbox has no filter by contact id, so the contact is matched by email address (sender or recipient): `email` plus any
13
+ * `alsoEmails`. A contact with no usable address has no conversations to show. SMS and LinkedIn threads are not matched here.
14
+ *
15
+ * Hidden threads: the API answers a thread the key may not see exactly like one that does not exist (404, the same text).
16
+ * A row that 404s on open says "not available to you" instead of an error with a retry button.
17
+ */
18
+
19
+ import { useState } from "react";
20
+ import { Button, Skeleton } from "@graph8/sdk/ui";
21
+ import { MessagesSquare } from "lucide-react";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { EmptyState } from "@/g8/components/empty-state";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { usePaginated } from "@/g8/hooks/use-paginated";
26
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
27
+ import { useInboxThread } from "../hooks/use-inbox-thread";
28
+ import { formatWhen, stripHtml, type InboxMessage } from "../lib/inbox-types";
29
+
30
+ const PAGE_SIZE = 10;
31
+
32
+ /** One thread of POST /inbox/emails/search. Only the fields shown here are read; all are optional. */
33
+ interface FoundThread { id?: string | number | null; email_id?: string | number | null; subject?: string | null; updated_at?: string | null; created_at?: string | null; status?: string | null; messages?: InboxMessage[] | null }
34
+
35
+ const idOf = (t: FoundThread): string => String(t.id ?? t.email_id ?? "");
36
+ const statusOf = (e: unknown): number | undefined => (e as { status?: number } | null)?.status;
37
+
38
+ /** Mailbox addresses from GET /inbox/mailboxes (rows are mailbox records; the address is `email`). */
39
+ function mailboxAddresses(env: unknown): string[] {
40
+ const data = (env as { data?: unknown } | null)?.data;
41
+ const rows = Array.isArray(data) ? data : ((data as { items?: unknown } | null)?.items ?? []);
42
+ const out = new Set<string>();
43
+ for (const r of Array.isArray(rows) ? rows : []) {
44
+ const row = r as { email?: string | null; mailbox?: string | null };
45
+ const address = (row.email ?? row.mailbox ?? "").trim();
46
+ if (address) out.add(address);
47
+ }
48
+ return [...out];
49
+ }
50
+
51
+ function Opened({ id }: { id: string }) {
52
+ const t = useInboxThread(id, "email");
53
+ if (t.error) {
54
+ return statusOf(t.error) === 404
55
+ ? <p role="status" className="p-3 text-sm text-muted-foreground">This conversation is not available to you. It may have been removed, or it belongs to a mailbox you cannot read.</p>
56
+ : <div className="p-3"><ApiError error={t.error} title="Could not load this conversation" onRetry={() => void t.refetch()} /></div>;
57
+ }
58
+ if (!t.data) return <Skeleton className="m-3 h-16 w-auto" aria-label="Loading conversation" />;
59
+ const messages = t.data.messages.filter((m) => !m.is_draft);
60
+ if (messages.length === 0) return <p className="p-3 text-sm text-muted-foreground">No messages in this conversation yet.</p>;
61
+ return (
62
+ <ol className="space-y-2 p-3" aria-label="Messages">
63
+ {messages.map((m, i) => (
64
+ <li key={m.message_id ?? i} className="rounded-md border p-3 text-sm">
65
+ <p className="mb-1 flex flex-wrap gap-2 text-xs text-muted-foreground"><span className="font-medium text-foreground">{m.from_address ?? "Unknown sender"}</span>{m.date ? <span>{formatWhen(m.date)}</span> : null}</p>
66
+ <p className="whitespace-pre-wrap">{stripHtml(m.content) || "(no content)"}</p>
67
+ </li>
68
+ ))}
69
+ </ol>
70
+ );
71
+ }
72
+
73
+ export interface RecordConversationsTabProps {
74
+ /** The contact's email address. Without one the tab explains there is nothing to match. */
75
+ email: string | null | undefined;
76
+ /** Other addresses of the contact (personal or additional emails; a comma-separated string is fine). Threads with any of them are shown. */
77
+ alsoEmails?: ReadonlyArray<string | null | undefined>;
78
+ /** Route to the conversation in your inbox screen; receives the inbox selection ("email:<id>"). Without it the rows link to `${inboxPath}?id=email:<id>`. */
79
+ onOpenThread?: (selection: string) => void;
80
+ /** Path of your inbox screen for the plain links. Default "/inbox". */
81
+ inboxPath?: string;
82
+ }
83
+
84
+ export function RecordConversationsTab({ email, alsoEmails = [], onOpenThread, inboxPath = "/inbox" }: RecordConversationsTabProps) {
85
+ const addresses = [...new Set([email, ...alsoEmails].flatMap((e) => (e ?? "").split(",")).map((e) => e.trim()).filter((e) => e !== "" && !e.includes("*") && e.includes("@")))];
86
+ const usable = addresses.length > 0;
87
+ const boxes = useG8Query(async (signal) => mailboxAddresses(await g8Get("/inbox/mailboxes", undefined, { signal })), [usable], { enabled: usable });
88
+ const mailboxes = boxes.data ?? [];
89
+
90
+ if (!usable) return <EmptyState compact icon={<MessagesSquare className="size-8" />} title="No email address" description="Conversations are matched by email address, and this contact has none to match." />;
91
+ if (boxes.error) return <ApiError error={boxes.error} title="Could not load mailboxes" onRetry={() => void boxes.refetch()} />;
92
+ if (!boxes.data) return <Skeleton className="h-24 w-full" aria-label="Loading conversations" />;
93
+ if (mailboxes.length === 0) return <EmptyState compact icon={<MessagesSquare className="size-8" />} title="No mailbox connected" description="Connect a mailbox to see the conversations you have with this contact." />;
94
+ // Mounted only once the mailboxes are known, so its first render is already "loading" (no empty-state flash before the first search).
95
+ return <ConversationList addresses={addresses} mailboxes={mailboxes} onOpenThread={onOpenThread} inboxPath={inboxPath} />;
96
+ }
97
+
98
+ interface ConversationListProps { addresses: string[]; mailboxes: string[]; onOpenThread?: (selection: string) => void; inboxPath: string }
99
+
100
+ function ConversationList({ addresses, mailboxes, onOpenThread, inboxPath }: ConversationListProps) {
101
+ const [open, setOpen] = useState<string | null>(null);
102
+ const list = usePaginated<FoundThread>(async (cursor, signal) => {
103
+ const page = cursor ? Number(cursor) || 1 : 1;
104
+ const env = await g8Post<{ items?: FoundThread[] }>(`/inbox/emails/search?page=${page}&page_size=${PAGE_SIZE}`, { mailboxes, search_emails: addresses }, { signal });
105
+ const items = (env.data?.items ?? []).filter((t) => idOf(t) !== "");
106
+ const more = (env as { pagination?: { has_next?: boolean } }).pagination?.has_next ?? items.length >= PAGE_SIZE;
107
+ return { data: items, pagination: { next_cursor: more && items.length > 0 ? String(page + 1) : null } } as never;
108
+ }, [addresses.join(","), mailboxes.join("|")]);
109
+
110
+ if (list.error) return <ApiError error={list.error} title="Could not load conversations" onRetry={() => void list.reload()} />;
111
+ if (list.loading && list.items.length === 0) return <Skeleton className="h-24 w-full" aria-label="Loading conversations" />;
112
+ if (list.items.length === 0) return <EmptyState compact icon={<MessagesSquare className="size-8" />} title="No conversations yet" description="Emails sent to or received from this contact show up here." />;
113
+
114
+ return (
115
+ <div className="space-y-2">
116
+ <ul className="divide-y rounded-md border" aria-label="Conversations">
117
+ {list.items.map((t) => {
118
+ const id = idOf(t);
119
+ const last = (t.messages ?? []).filter((m) => !m.is_draft).slice(-1)[0];
120
+ const isOpen = open === id;
121
+ const selection = `email:${id}`;
122
+ return (
123
+ <li key={id}>
124
+ <div className="flex items-center justify-between gap-3 p-3 text-sm">
125
+ <button type="button" aria-expanded={isOpen} className="min-w-0 flex-1 text-left" onClick={() => setOpen(isOpen ? null : id)}>
126
+ <span className="block truncate font-medium">{t.subject || "(no subject)"}</span>
127
+ <span className="block truncate text-xs text-muted-foreground">{[formatWhen(t.updated_at ?? t.created_at), last ? stripHtml(last.content).slice(0, 80) : null].filter(Boolean).join(" · ")}</span>
128
+ </button>
129
+ <a
130
+ href={`${inboxPath}?id=${encodeURIComponent(selection)}`}
131
+ className="shrink-0 text-xs text-muted-foreground hover:underline"
132
+ onClick={(e) => {
133
+ if (!onOpenThread || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
134
+ e.preventDefault();
135
+ onOpenThread(selection);
136
+ }}
137
+ >
138
+ Open in inbox
139
+ </a>
140
+ </div>
141
+ {isOpen ? <Opened id={id} /> : null}
142
+ </li>
143
+ );
144
+ })}
145
+ </ul>
146
+ {list.hasMore ? (
147
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
148
+ <span role="status">Showing {list.items.length.toLocaleString()} conversations</span>
149
+ <Button size="sm" variant="outline" loading={list.loadingMore} onClick={() => void list.loadMore()}>Load more</Button>
150
+ </div>
151
+ ) : null}
152
+ </div>
153
+ );
154
+ }
@@ -0,0 +1,136 @@
1
+ "use client";
2
+
3
+ /**
4
+ * TagManager: the inbox tag library. See every tag, create one, rename or recolour one, delete one.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. GET /workflows/inbox-tags (workflows.listInboxTags) READ - every tag, with its built-in policy
8
+ * 2. POST /inbox/tags (aiInbox.createInboxTag) write
9
+ * 3. PUT /inbox/tags/{tag_id} (aiInbox.updateInboxTag) write - ONLY the fields that changed
10
+ * 4. DELETE /inbox/tags/{tag_id} (aiInbox.deleteInboxTag) DESTRUCTIVE -> ConfirmSpendDialog first
11
+ *
12
+ * Built-in tags follow the API's policy: "system" tags cannot be edited or deleted, "sdr_ai" tags cannot be renamed
13
+ * or deleted. Deleting a tag removes the label from every conversation that carries it (nothing else is removed).
14
+ * The edit form is seeded from a LOADED row and its Save stays disabled until something changed; it never writes a
15
+ * tag that was not loaded, and the request carries only the changed fields (a partial PUT must not reset
16
+ * `ai_can_apply` or the rules, which this form does not show). Applying a tag to a conversation is TagAndAssign.
17
+ */
18
+ import { useEffect, useState } from "react";
19
+ import { Pencil, Plus, Trash2 } from "lucide-react";
20
+ import { Button, Input, Skeleton } from "@graph8/sdk/ui";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
+ import { EmptyState } from "@/g8/components/empty-state";
24
+ import { StatusPill } from "@/g8/components/screen/status-pill";
25
+ import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
26
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
+ import type { InboxTag } from "../lib/inbox-types";
29
+ import { asInboxTags, canDeleteTag, canEditTag, canRenameTag, isHexColor, managedTags, tagChanges } from "../lib/inbox-manage";
30
+ import type { ManagedTag } from "../lib/inbox-manage";
31
+
32
+ export interface TagManagerProps {
33
+ /** Called with the current tag list after every load and change, so a screen can keep its pickers in step. */
34
+ onTags?: (tags: InboxTag[]) => void;
35
+ className?: string;
36
+ }
37
+
38
+ function TagForm({ tag, onDone }: { tag: ManagedTag | null; onDone: (changed: boolean) => void }) {
39
+ const [name, setName] = useState(tag?.name ?? "");
40
+ const [description, setDescription] = useState(tag?.description ?? "");
41
+ const [color, setColor] = useState(tag?.color ?? "");
42
+ useEffect(() => {
43
+ setName(tag?.name ?? "");
44
+ setDescription(tag?.description ?? "");
45
+ setColor(tag?.color ?? "");
46
+ }, [tag?.id]); // eslint-disable-line react-hooks/exhaustive-deps
47
+ const changes = tag ? tagChanges(tag, { name, description, color }) : {};
48
+ const save = useG8Mutation(async () => {
49
+ if (tag) await g8Put(`/inbox/tags/${encodeURIComponent(tag.id)}`, changes);
50
+ else await g8Post("/inbox/tags", { name: name.trim(), ...(description.trim() ? { description: description.trim() } : {}), ...(color.trim() ? { color: color.trim() } : {}) });
51
+ }, { invalidates: ["inbox"] });
52
+ const valid = name.trim().length > 0 && isHexColor(color) && (tag ? Object.keys(changes).length > 0 : true);
53
+ const submit = async () => {
54
+ try {
55
+ await save.mutateAsync(undefined);
56
+ onDone(true);
57
+ } catch {
58
+ /* the error renders in the form; the typed values stay */
59
+ }
60
+ };
61
+ return (
62
+ <form className="space-y-2 rounded-md border border-border p-3" onSubmit={(e) => { e.preventDefault(); if (valid) void submit(); }} aria-label={tag ? `Edit ${tag.name}` : "New tag"}>
63
+ <Input aria-label="Tag name" placeholder="Tag name" value={name} disabled={tag ? !canRenameTag(tag) : false} onChange={(e) => setName(e.target.value)} />
64
+ <Input aria-label="Description" placeholder="What this tag means (optional)" value={description} onChange={(e) => setDescription(e.target.value)} />
65
+ <div className="flex items-center gap-2">
66
+ <Input aria-label="Colour" className="w-32" placeholder="#eab308" value={color} onChange={(e) => setColor(e.target.value)} />
67
+ {!isHexColor(color) ? <span role="alert" className="text-xs text-error-foreground">Use a hex colour such as #eab308.</span> : null}
68
+ </div>
69
+ {save.error ? <ApiError error={save.error} title={tag ? "Could not save the tag" : "Could not create the tag"} /> : null}
70
+ <div className="flex justify-end gap-2">
71
+ <Button type="button" variant="outline" size="sm" onClick={() => onDone(false)}>Cancel</Button>
72
+ <Button type="submit" size="sm" disabled={!valid || save.loading}>{tag ? "Save changes" : "Create tag"}</Button>
73
+ </div>
74
+ </form>
75
+ );
76
+ }
77
+
78
+ export function TagManager({ onTags, className }: TagManagerProps) {
79
+ const [editing, setEditing] = useState<ManagedTag | "new" | null>(null);
80
+ const [deleting, setDeleting] = useState<ManagedTag | null>(null);
81
+ const tags = useG8Query(async (signal) => managedTags(await g8Get("/workflows/inbox-tags", undefined, { signal })), [], { key: ["inbox", "tags"] });
82
+ useEffect(() => { if (tags.data) onTags?.(asInboxTags(tags.data)); }, [tags.data]); // eslint-disable-line react-hooks/exhaustive-deps
83
+ const remove = useG8Mutation(async (tag: ManagedTag) => {
84
+ await g8Delete(`/inbox/tags/${encodeURIComponent(tag.id)}`);
85
+ setDeleting(null);
86
+ await tags.refetch();
87
+ }, { invalidates: ["inbox"], onError: () => setDeleting(null) });
88
+
89
+ if (tags.error) return <ApiError error={tags.error} title="Could not load the tags" onRetry={() => void tags.refetch()} className={className} />;
90
+ if (tags.loading && !tags.data) return <div aria-busy="true" className={`space-y-2 ${className ?? ""}`}><Skeleton className="h-10 w-full" /><Skeleton className="h-10 w-full" /></div>;
91
+ const rows = tags.data ?? [];
92
+ const done = (changed: boolean) => { setEditing(null); if (changed) void tags.refetch(); };
93
+
94
+ return (
95
+ <div className={`space-y-3 ${className ?? ""}`}>
96
+ <div className="flex items-center justify-between">
97
+ <h3 className="font-medium">Tags</h3>
98
+ <Button size="sm" variant="outline" disabled={editing != null} onClick={() => setEditing("new")}><Plus className="mr-1 size-4" aria-hidden />New tag</Button>
99
+ </div>
100
+ {editing === "new" ? <TagForm tag={null} onDone={done} /> : null}
101
+ {rows.length === 0 && editing !== "new" ? (
102
+ <EmptyState compact title="No tags yet" description="Tags label conversations so you can filter and route them." action={<Button size="sm" onClick={() => setEditing("new")}>Create a tag</Button>} />
103
+ ) : (
104
+ <ul aria-label="Tags" className="divide-y rounded-md border border-border">
105
+ {rows.map((t) => (
106
+ <li key={t.id} className="p-2 text-sm">
107
+ {editing !== "new" && editing?.id === t.id ? (
108
+ <TagForm tag={t} onDone={done} />
109
+ ) : (
110
+ <div className="flex items-center gap-2">
111
+ <span aria-hidden className="size-3 shrink-0 rounded-full border border-border" style={t.color ? { backgroundColor: t.color } : undefined} />
112
+ <span className="min-w-0 flex-1"><span className="font-medium">{t.name}</span>{t.description ? <span className="block truncate text-xs text-muted-foreground">{t.description}</span> : null}</span>
113
+ {t.policy !== "user" ? <StatusPill tone="neutral">built in</StatusPill> : null}
114
+ <Button size="sm" variant="ghost" aria-label={`Edit ${t.name}`} disabled={!canEditTag(t) || editing != null} onClick={() => setEditing(t)}><Pencil className="size-4" aria-hidden /></Button>
115
+ <Button size="sm" variant="ghost" aria-label={`Delete ${t.name}`} disabled={!canDeleteTag(t)} onClick={() => setDeleting(t)}><Trash2 className="size-4" aria-hidden /></Button>
116
+ </div>
117
+ )}
118
+ </li>
119
+ ))}
120
+ </ul>
121
+ )}
122
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the tag" /> : null}
123
+ <ConfirmSpendDialog
124
+ open={deleting != null}
125
+ tier="destructive"
126
+ title={`Delete the tag "${deleting?.name ?? ""}"?`}
127
+ description="Every conversation loses this label. The conversations themselves are not touched. This cannot be undone."
128
+ confirmLabel="Delete tag"
129
+ affected={1}
130
+ confirming={remove.loading}
131
+ onConfirm={() => deleting && void remove.mutate(deleting)}
132
+ onCancel={() => setDeleting(null)}
133
+ />
134
+ </div>
135
+ );
136
+ }