@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
package/README.md CHANGED
@@ -512,6 +512,25 @@ npx @graph8/sdk create mailchimp my-app # then: cd my-app && cp .env.local.exa
512
512
 
513
513
  Every available stack is proven by `npm run build && npm run e2e:create -- <stack>`, which packs the SDK, scaffolds the app with the built CLI, installs it and requires `next build` to pass. More stacks appear here as their blocks land; `create` refuses a stack that is not available yet.
514
514
 
515
+ ### Branding: your Brand Kit, automatically
516
+
517
+ If your graph8 organization already has a Brand Kit (Studio), `create` uses it. With a key in `G8_API_KEY` (environment or `.env.local`), the new app gets your organization's name, primary colour and logo without any flags; the logo is downloaded into `public/brand/` (never hot-linked), and the colour becomes `--brand` / `--primary` with button text checked for WCAG AA.
518
+
519
+ ```bash
520
+ G8_API_KEY=... npx @graph8/sdk create apollo my-app # picks up the Brand Kit
521
+ npx @graph8/sdk create apollo my-app --no-org-brand # skip it
522
+ npx @graph8/sdk create apollo my-app --brand-color "#0f62fe" # your colour wins; the kit still fills the name and logo
523
+ npx @graph8/sdk brand --from-org # brand an existing app from the kit
524
+ ```
525
+
526
+ | Brand Kit field (Developer API) | App brand input |
527
+ |---|---|
528
+ | `GET /me` `org_name` | `--name` (`g8.config.ts` `name`) |
529
+ | `GET /brand-kit` `design_tokens.colors.primary` (else `brand`, `accent`, button background) | `--brand-color` |
530
+ | `GET /brand-kit` `logo_url` (one logo; no light, dark or icon variant) | `--logo` (`public/brand/logo.*`) |
531
+
532
+ Explicit flags always win, field by field. With no key, a missing or empty kit, no `campaigns:read` scope or no network, the app keeps the default look and the CLI prints one info line; it never fails. The key is never printed or written. Fonts, tagline and voice traits are in the kit but have no brand input, so they are not applied. Icons stay Lucide.
533
+
515
534
  ## UI kit (`@graph8/sdk/ui`)
516
535
 
517
536
  The React component kit and design tokens behind the graph8 app (primitives, light and dark
@@ -238,6 +238,20 @@ Rules the kit encodes, so screens stay consistent:
238
238
  - Loading is visible and distinct from empty: `StatStrip loading`, `Skeleton` rows.
239
239
  - Dates show short text; the full timestamp is the `title` of a `<time dateTime>`.
240
240
 
241
+ ## Long lists: paging, notices and filters (no silent truncation)
242
+
243
+ A list read that sends a `limit` must page or say it stopped. The foundation ships the pieces:
244
+
245
+ | Need | Use |
246
+ |---|---|
247
+ | Show rows with a "Load more" button (the API pages by `page` or `offset`) | `useLoadMoreList(byPage(size, read))` or `byOffset(size, read)` (`lib/paging.ts`), then `<LoadMore>` under the rows |
248
+ | The API has only a `limit` (no paging) | `<FirstNotice shown={rows.length} limit={LIMIT} noun="mailboxes" />`: "Showing the first N ..." appears only when the read came back full |
249
+ | A screen needs EVERY row (a lookup by id, a full dropdown) | `useAllPages(read, deps)`: reads page after page, reports `truncated` if the page cap cut it short |
250
+ | Choose one audience list | crm's `ListSelect`: a native select with server search and "Load more lists" |
251
+ | Filters for a list screen | `useUrlFilters(keys, { prefix })` keeps them in the URL (`c_email=...`); `TextFilter`, `SelectFilter`, `RangeFilter` and `MoreFilters` (a "More filters" disclosure) go in the Toolbar `filters` slot |
252
+
253
+ `tests/blocks/list-paging-guard.test.ts` in the SDK repo fails on a new `g8Get(..., { limit })` read that does none of these.
254
+
241
255
  ## Prerequisites
242
256
 
243
257
  - `G8_API_KEY` in the server env; verify with `recipes/verify-proxy-key.ts`.
@@ -155,6 +155,27 @@
155
155
  "purpose": "White-label context: the app's name, logo, and what the platform is called and where it lives, for every block's copy and link-outs. Fields left out stay graph8's.",
156
156
  "operations": [],
157
157
  "tiers": []
158
+ },
159
+ {
160
+ "name": "ListSelect",
161
+ "file": "components/list-select.tsx",
162
+ "purpose": "Audience-list dropdown, searchable and paged (native select + server search + Load more). It owns no API call: the block that uses it passes a `read` (crm ships ListSelect with the /lists read wired in).",
163
+ "operations": [],
164
+ "tiers": []
165
+ },
166
+ {
167
+ "name": "FirstNotice",
168
+ "file": "components/paging-notices.tsx",
169
+ "purpose": "FirstNotice (\"Showing the first N ...\" when a read without paging came back full) and LoadMore (\"Showing N\" + Load more): the two honest endings of a capped list.",
170
+ "operations": [],
171
+ "tiers": []
172
+ },
173
+ {
174
+ "name": "TextFilter",
175
+ "file": "components/screen/filter-controls.tsx",
176
+ "purpose": "Toolbar filter controls: TextFilter, SelectFilter, RangeFilter (from/to) and MoreFilters (a disclosure that opens by itself when a hidden filter is set).",
177
+ "operations": [],
178
+ "tiers": []
158
179
  }
159
180
  ],
160
181
  "hooks": [
@@ -192,6 +213,21 @@
192
213
  "name": "useBrand",
193
214
  "file": "hooks/use-brand.ts",
194
215
  "purpose": "The current brand ({ name, appUrl, linkLabel, logo }) from the nearest BrandProvider, else graph8's defaults; brandUrl(brand, path) builds a link-out."
216
+ },
217
+ {
218
+ "name": "useAllPages",
219
+ "file": "hooks/use-all-pages.ts",
220
+ "purpose": "Read every page of a list a screen needs in full (lookups, full dropdowns), up to a page cap, and report when the cap cut it short."
221
+ },
222
+ {
223
+ "name": "useLoadMoreList",
224
+ "file": "hooks/use-load-more-list.ts",
225
+ "purpose": "A list read with Load more: the shape of a useG8Query list (data, loading, error, refetch) plus hasMore / loadMore. Pair with byPage / byOffset and the LoadMore notice."
226
+ },
227
+ {
228
+ "name": "useUrlFilters",
229
+ "file": "hooks/use-url-filters.ts",
230
+ "purpose": "List-screen filters kept in the URL (debounced, prefixed, SSR-safe): draft for the inputs, query for the read, clear()."
195
231
  }
196
232
  ],
197
233
  "lib": [
@@ -218,6 +254,14 @@
218
254
  {
219
255
  "file": "lib/plural.ts",
220
256
  "purpose": "plural(n, 'contact') -> '1 contact' / '2 contacts' (and pluralWord): one correct plural helper for titles, counts and bulk dialogs."
257
+ },
258
+ {
259
+ "file": "lib/paging.ts",
260
+ "purpose": "byPage / byOffset adapt page-number and offset lists to usePaginated's cursor shape; hasNextOf reads the API's has_next / next_cursor."
261
+ },
262
+ {
263
+ "file": "lib/use-debounced-value.ts",
264
+ "purpose": "useDebounced: a value after it stopped changing (foundation-local copy)."
221
265
  }
222
266
  ],
223
267
  "server": [
@@ -39,6 +39,13 @@ const TIER_COPY: Record<ConfirmTier, { badge: string; confirm: string; note: str
39
39
  };
40
40
 
41
41
  /** True for the tiers an app should confirm before calling. read/write never need it. */
42
+ /** The tier badge: ember for what cannot be undone, ochre for spend and outreach. Token classes only. */
43
+ const TIER_BADGE: Record<ConfirmTier, string> = {
44
+ billable: "bg-warning text-warning-foreground",
45
+ external: "bg-warning text-warning-foreground",
46
+ destructive: "bg-error text-error-foreground",
47
+ };
48
+
42
49
  export function tierRequiresConfirmation(tier: string): tier is ConfirmTier {
43
50
  return tier === "billable" || tier === "external" || tier === "destructive";
44
51
  }
@@ -83,7 +90,7 @@ export function ConfirmSpendDialog(props: ConfirmSpendDialogProps) {
83
90
  showCloseButton={false}
84
91
  data-tier={tier}
85
92
  style={interactive}
86
- className={`max-w-md sm:max-w-md bg-white p-5 shadow-xl ${props.className ?? ""}`}
93
+ className={`max-w-md gap-4 border-border bg-background p-5 shadow-xl sm:max-w-md ${props.className ?? ""}`}
87
94
  onOpenAutoFocus={(event) => {
88
95
  event.preventDefault();
89
96
  openerRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
@@ -98,15 +105,15 @@ export function ConfirmSpendDialog(props: ConfirmSpendDialogProps) {
98
105
  onInteractOutside={(event) => event.preventDefault()}
99
106
  onEscapeKeyDown={(event) => confirming && event.preventDefault()}
100
107
  >
101
- <span className="inline-block w-fit rounded bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700">
108
+ <span className={`inline-flex h-5 w-fit items-center rounded-full px-2 text-[11px] font-medium leading-none ${TIER_BADGE[tier]}`}>
102
109
  {copy.badge}
103
110
  </span>
104
- <DialogTitle className="text-base font-semibold text-gray-900">{title}</DialogTitle>
111
+ <DialogTitle className="text-base font-medium leading-6 text-foreground">{title}</DialogTitle>
105
112
  <DialogDescription asChild>
106
- <div className="space-y-2 text-sm text-gray-600">
113
+ <div className="space-y-2 text-sm leading-5 text-muted-foreground">
107
114
  {description ? <div>{description}</div> : null}
108
115
  {summary.length > 0 ? (
109
- <ul className="list-disc pl-5 text-gray-900">
116
+ <ul className="list-disc space-y-0.5 pl-5 tabular-nums text-foreground">
110
117
  {summary.map((line) => (
111
118
  <li key={line}>{line}</li>
112
119
  ))}
@@ -115,14 +122,14 @@ export function ConfirmSpendDialog(props: ConfirmSpendDialogProps) {
115
122
  <p>{copy.note}</p>
116
123
  </div>
117
124
  </DialogDescription>
118
- <div className="flex justify-end gap-2">
125
+ <div className="flex justify-end gap-2 pt-1">
119
126
  <button
120
127
  ref={cancelRef}
121
128
  type="button"
122
129
  disabled={confirming}
123
130
  onClick={onCancel}
124
131
  style={interactive}
125
- className="rounded border border-gray-300 px-3 py-1.5 text-sm text-gray-700 disabled:opacity-50"
132
+ className="inline-flex h-8 items-center rounded-md border border-border bg-background px-3 text-sm font-medium text-foreground transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
126
133
  >
127
134
  {props.cancelLabel ?? "Cancel"}
128
135
  </button>
@@ -131,7 +138,7 @@ export function ConfirmSpendDialog(props: ConfirmSpendDialogProps) {
131
138
  disabled={confirming}
132
139
  onClick={onConfirm}
133
140
  style={interactive}
134
- className="rounded bg-gray-900 px-3 py-1.5 text-sm font-medium text-white disabled:opacity-50"
141
+ className={`inline-flex h-8 items-center rounded-md px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:opacity-50 ${tier === "destructive" ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : "bg-primary text-primary-foreground hover:bg-primary/90"}`}
135
142
  >
136
143
  {confirming ? "Working..." : (props.confirmLabel ?? copy.confirm)}
137
144
  </button>
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import type { ReactNode } from "react";
4
+ import { Inbox } from "lucide-react";
4
5
  import { cn } from "@graph8/sdk/ui";
5
6
 
6
7
  export interface EmptyStateProps {
@@ -24,6 +25,8 @@ export interface EmptyStateProps {
24
25
  * these filters.", with a Clear filters action). An ink icon in a quiet well, one sentence, one action.
25
26
  */
26
27
  export function EmptyState({ title, description, action, icon, className, secondaryAction, compact, bordered }: EmptyStateProps) {
28
+ // No icon passed: a quiet default, so an empty area never reads as a blank page. `icon={null}` hides it.
29
+ const mark = icon === undefined ? <Inbox strokeWidth={1.5} /> : icon;
27
30
  return (
28
31
  <div
29
32
  role="status"
@@ -35,12 +38,12 @@ export function EmptyState({ title, description, action, icon, className, second
35
38
  className,
36
39
  )}
37
40
  >
38
- {icon ? (
41
+ {mark ? (
39
42
  <div
40
43
  aria-hidden
41
44
  className="mb-1 flex size-10 items-center justify-center rounded-lg border border-border bg-muted text-foreground [&_svg]:size-5 [&_svg]:stroke-[1.5]"
42
45
  >
43
- {icon}
46
+ {mark}
44
47
  </div>
45
48
  ) : null}
46
49
  <h3 className="text-sm font-medium text-foreground">{title}</h3>
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ListSelect: the audience-list dropdown, searchable and paged. It replaces a `<select>` fed by one capped
5
+ * `GET /lists?limit=200` read, which could not reach the 201st list. It is still a native `<select>` (keyboard,
6
+ * mobile and form behaviour unchanged, and the same accessible name the old select had), now with:
7
+ *
8
+ * - paging: `pageSize` lists per read (default 100); "Load more lists" appends the next page, nothing is dropped;
9
+ * - search: once there is more than one page, a "Search lists" box looks lists up on the server (`search=`);
10
+ * - the chosen list always stays an option, even when it is not on a loaded page (`valueLabel` names it).
11
+ *
12
+ * The paging engine is useLoadMoreList (the same byPage / hasNextOf contract ContactPicker's usePickerPages follows).
13
+ * It lives in foundation, not in the opt-in pickers block, so crm / forms / newsletter / nurture / sequencer can use
14
+ * it without a new block dependency.
15
+ *
16
+ * SDK operations: none here. The foundation holds no API scope of its own, so the list read comes in as `read`; the crm block
17
+ * ships a ready-made `ListSelect` (@/g8/crm/components/list-select) that passes GET /lists?search&page&limit (listLists, read).
18
+ */
19
+ import { useEffect, useRef, useState } from "react";
20
+ import { Button, Input } from "@graph8/sdk/ui";
21
+ import { byPage } from "../lib/paging";
22
+ import type { RowsPage } from "../lib/paging";
23
+ import { useLoadMoreList } from "../hooks/use-load-more-list";
24
+ import { ApiError } from "./api-error";
25
+
26
+ /** A list as a select option. */
27
+ export interface ListSelectOption {
28
+ id: number;
29
+ title: string;
30
+ type?: string | null;
31
+ contact_count?: number | null;
32
+ company_count?: number | null;
33
+ total?: number | null;
34
+ }
35
+
36
+ /** Reads one page of lists: `q` is the server search, `page` starts at 1. */
37
+ export type ListsReader = (q: string, page: number, pageSize: number, signal: AbortSignal) => Promise<RowsPage<ListSelectOption>>;
38
+
39
+ export interface ListSelectProps {
40
+ /** Reads a page of lists (see crm's ListSelect, which wires GET /lists). */
41
+ read: ListsReader;
42
+ /** The chosen list id (controlled); "" / null / undefined = none. */
43
+ value?: number | string | null;
44
+ /** Called with the chosen list, or null when the blank option is chosen. */
45
+ onChange: (list: ListSelectOption | null) => void;
46
+ /** Accessible name of the select (what the old select was called). */
47
+ label: string;
48
+ /** Text of the blank option ("Audience list (required)"). */
49
+ emptyOption?: string;
50
+ /** Option text for a list; default the title. */
51
+ optionLabel?: (list: ListSelectOption) => string;
52
+ /** Hide lists of the other type. Default: every list. */
53
+ type?: "contacts" | "companies";
54
+ /** Lists per read. Default 100. */
55
+ pageSize?: number;
56
+ /** Name for `value` before its list has been loaded. */
57
+ valueLabel?: string;
58
+ /** Hold the read (for example until a wizard step is reached). Default true. */
59
+ enabled?: boolean;
60
+ disabled?: boolean;
61
+ /** Id of the select, so a <label htmlFor> can name it. */
62
+ id?: string;
63
+ className?: string;
64
+ /** Class of the select element itself. */
65
+ selectClassName?: string;
66
+ }
67
+
68
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
69
+
70
+ export function ListSelect(props: ListSelectProps) {
71
+ const { read, value, onChange, label, emptyOption = "Choose a list", optionLabel, type, pageSize = 100, valueLabel, enabled = true, disabled, id, className, selectClassName } = props;
72
+ const [search, setSearch] = useState("");
73
+ const [q, setQ] = useState("");
74
+ useEffect(() => {
75
+ const t = setTimeout(() => setQ(search.trim()), 250);
76
+ return () => clearTimeout(t);
77
+ }, [search]);
78
+ const list = useLoadMoreList(byPage<ListSelectOption>(pageSize, async (page, signal) => {
79
+ const got = await read(q, page, pageSize, signal);
80
+ return { rows: got.rows.filter((l) => !type || !l.type || l.type === type), hasNext: got.hasNext };
81
+ }), [q, pageSize, type], { enabled });
82
+ // A page that held only lists of the other type is empty after the filter but the server may have more: keep going.
83
+ useEffect(() => { if (list.data && list.data.length === 0 && list.hasMore && !list.loadingMore && !list.loading) list.loadMore(); }, [list.data, list.hasMore, list.loadingMore, list.loading]); // eslint-disable-line react-hooks/exhaustive-deps
84
+ const pages = { rows: list.data ?? [], hasMore: list.hasMore, loadingMore: list.loadingMore, loadMore: list.loadMore, error: list.error, refetch: () => { void list.refetch(); }, q, search, setSearch };
85
+ const key = value == null ? "" : String(value);
86
+ const rows = pages.rows;
87
+ const known = rows.some((l) => String(l.id) === key);
88
+ const text = optionLabel ?? ((l: ListSelectOption) => l.title);
89
+ // The search box appears once the lists run past one page and then stays, so it does not vanish under the cursor.
90
+ const seenMore = useRef(false);
91
+ if (pages.hasMore) seenMore.current = true;
92
+ const searchable = seenMore.current || pages.q !== "" || pages.search !== "";
93
+ return (
94
+ <div className={`flex flex-col gap-1 ${className ?? ""}`}>
95
+ {searchable ? <Input type="text" aria-label="Search lists" placeholder="Search lists" value={pages.search} onChange={(e) => pages.setSearch(e.target.value)} className="h-8 text-sm" /> : null}
96
+ <select
97
+ id={id}
98
+ aria-label={label}
99
+ className={selectClassName ?? SELECT}
100
+ value={key}
101
+ disabled={disabled}
102
+ onChange={(e) => onChange(rows.find((l) => String(l.id) === e.target.value) ?? null)}
103
+ >
104
+ <option value="">{emptyOption}</option>
105
+ {key && !known ? <option value={key}>{valueLabel ?? `List ${key}`}</option> : null}
106
+ {rows.map((l) => <option key={l.id} value={l.id}>{text(l)}</option>)}
107
+ </select>
108
+ {pages.error ? <ApiError error={pages.error} title="Could not load lists" onRetry={pages.refetch} /> : null}
109
+ {pages.hasMore ? <Button type="button" variant="ghost" size="sm" loading={pages.loadingMore} onClick={pages.loadMore}>Load more lists</Button> : null}
110
+ </div>
111
+ );
112
+ }
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ /**
4
+ * FirstNotice and LoadMore: the two honest answers to a list that is longer than one read.
5
+ *
6
+ * - LoadMore: the API pages (page / offset / cursor): show how many rows are on screen and a "Load more" button.
7
+ * - FirstNotice: the API has no paging for this list: when the read came back full, say "Showing the first N".
8
+ *
9
+ * A capped list must never end silently; one of these is on every list read that sends a limit.
10
+ * SDK operations: none.
11
+ */
12
+ import { Button, cn } from "@graph8/sdk/ui";
13
+
14
+ export interface FirstNoticeProps {
15
+ /** Rows on screen. */
16
+ shown: number;
17
+ /** The limit the read was sent with. */
18
+ limit: number;
19
+ /** What the rows are, plural: "mailboxes". */
20
+ noun: string;
21
+ /** Decide it yourself when the loader knows rows were cut off (useAllPages `truncated`): true shows, false hides, whatever the counts. */
22
+ force?: boolean;
23
+ className?: string;
24
+ }
25
+
26
+ /** Renders only when `shown` reached `limit` (a full read: there may be more), or when `force` says so. */
27
+ export function FirstNotice({ shown, limit, noun, force, className }: FirstNoticeProps) {
28
+ if (!(force ?? shown >= limit)) return null;
29
+ return (
30
+ <p role="note" className={cn("text-xs text-muted-foreground", className)}>
31
+ Showing the first {(force ? shown : limit).toLocaleString()} {noun}. There may be more: narrow the list with a search or a filter to find the rest.
32
+ </p>
33
+ );
34
+ }
35
+
36
+ export interface LoadMoreProps {
37
+ /** Rows on screen. */
38
+ shown: number;
39
+ noun: string;
40
+ hasMore: boolean;
41
+ loading?: boolean;
42
+ onLoadMore: () => void;
43
+ className?: string;
44
+ }
45
+
46
+ /** "Showing 100 landing pages" with a "Load more" button while the API has another page. */
47
+ export function LoadMore({ shown, noun, hasMore, loading, onLoadMore, className }: LoadMoreProps) {
48
+ if (!hasMore) return null;
49
+ return (
50
+ <div className={cn("flex items-center justify-between gap-3 py-2 text-sm text-muted-foreground", className)}>
51
+ <span role="status">Showing {shown.toLocaleString()} {noun}</span>
52
+ <Button type="button" variant="outline" size="sm" loading={loading} onClick={onLoadMore}>Load more</Button>
53
+ </div>
54
+ );
55
+ }
@@ -71,7 +71,6 @@ export function DetailPanel({ title, subtitle, avatar, badges, actions, tabs, ac
71
71
  {badges ? <div className="mt-1.5 flex flex-wrap items-center gap-1.5">{badges}</div> : null}
72
72
  </div>
73
73
  <div className="flex shrink-0 items-center gap-1">
74
- {actions}
75
74
  {onCollapse ? (
76
75
  <Button type="button" variant="ghost" size="icon" className="size-8 text-text-color-muted hover:text-foreground" aria-label="Back to split view" onClick={onCollapse}>
77
76
  <Minimize2 className="size-4" strokeWidth={1.5} aria-hidden />
@@ -88,6 +87,8 @@ export function DetailPanel({ title, subtitle, avatar, badges, actions, tabs, ac
88
87
  ) : null}
89
88
  </div>
90
89
  </div>
90
+ {/* Record actions get their own row, so the record's name is never squeezed out by them. */}
91
+ {actions ? <div className="mt-3 flex flex-wrap items-center gap-1.5" data-testid="detail-panel-actions">{actions}</div> : null}
91
92
  {summary ? <div className="mt-3">{summary}</div> : null}
92
93
  </header>
93
94
  {tabs && tabs.length > 0 ? (
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Filter controls for a Toolbar: a text box, a select, a from/to range, and a "More filters" disclosure that keeps the
5
+ * toolbar to one row until a person asks for the rest (it opens by itself when a hidden filter is already set).
6
+ * They are plain controlled inputs; useUrlFilters keeps their values in the URL.
7
+ *
8
+ * SDK operations: none.
9
+ */
10
+ import { useEffect, useState, type ReactNode } from "react";
11
+ import { Button, cn } from "@graph8/sdk/ui";
12
+
13
+ const BOX = "h-9 rounded-md border bg-background px-3 text-sm";
14
+
15
+ export interface TextFilterProps {
16
+ label: string;
17
+ value: string;
18
+ onChange: (value: string) => void;
19
+ placeholder?: string;
20
+ type?: "text" | "number" | "date" | "email";
21
+ className?: string;
22
+ }
23
+
24
+ /** One text box; the accessible name is "Filter by <label>". */
25
+ export function TextFilter({ label, value, onChange, placeholder, type = "text", className }: TextFilterProps) {
26
+ return <input type={type} aria-label={`Filter by ${label}`} placeholder={placeholder ?? label} className={cn(BOX, "w-36", className)} value={value} onChange={(e) => onChange(e.target.value)} />;
27
+ }
28
+
29
+ export interface SelectFilterProps {
30
+ label: string;
31
+ value: string;
32
+ onChange: (value: string) => void;
33
+ /** Text of the "no filter" option, for example "Any seniority". */
34
+ any: string;
35
+ options: ReadonlyArray<string | { value: string; label: string }>;
36
+ className?: string;
37
+ }
38
+
39
+ /** One select; the accessible name is "Filter by <label>". */
40
+ export function SelectFilter({ label, value, onChange, any, options, className }: SelectFilterProps) {
41
+ return (
42
+ <select aria-label={`Filter by ${label}`} className={cn(BOX, "px-2", className)} value={value} onChange={(e) => onChange(e.target.value)}>
43
+ <option value="">{any}</option>
44
+ {options.map((o) => { const v = typeof o === "string" ? o : o.value; return <option key={v} value={v}>{typeof o === "string" ? o : o.label}</option>; })}
45
+ </select>
46
+ );
47
+ }
48
+
49
+ export interface RangeFilterProps {
50
+ label: string;
51
+ min: string;
52
+ max: string;
53
+ onMinChange: (value: string) => void;
54
+ onMaxChange: (value: string) => void;
55
+ type?: "number" | "date";
56
+ className?: string;
57
+ }
58
+
59
+ /** A from / to pair; the accessible names are "<label> from" and "<label> to". */
60
+ export function RangeFilter({ label, min, max, onMinChange, onMaxChange, type = "number", className }: RangeFilterProps) {
61
+ return (
62
+ <span role="group" aria-label={label} className={cn("flex items-center gap-1", className)}>
63
+ <input type={type} aria-label={`${label} from`} placeholder={`${label} min`} className={cn(BOX, "w-28")} value={min} onChange={(e) => onMinChange(e.target.value)} />
64
+ <span className="text-muted-foreground text-xs" aria-hidden>to</span>
65
+ <input type={type} aria-label={`${label} to`} placeholder={`${label} max`} className={cn(BOX, "w-28")} value={max} onChange={(e) => onMaxChange(e.target.value)} />
66
+ </span>
67
+ );
68
+ }
69
+
70
+ export interface MoreFiltersProps {
71
+ /** How many of the hidden filters are set (shown on the button, and opens the panel when it is above zero at mount). */
72
+ activeCount: number;
73
+ children: ReactNode;
74
+ label?: string;
75
+ className?: string;
76
+ }
77
+
78
+ /** A "More filters" button and the second row of controls it reveals. Put it in Toolbar `filters`. */
79
+ export function MoreFilters({ activeCount, children, label = "More filters", className }: MoreFiltersProps) {
80
+ const [open, setOpen] = useState(activeCount > 0);
81
+ // Filters adopted from the URL right after mount (a reload, a shared link) open the panel too.
82
+ const hasActive = activeCount > 0;
83
+ useEffect(() => { if (hasActive) setOpen(true); }, [hasActive]);
84
+ return (
85
+ <>
86
+ <Button type="button" variant="outline" size="sm" aria-expanded={open} onClick={() => setOpen((o) => !o)}>
87
+ {label}{activeCount > 0 ? ` (${activeCount})` : ""}
88
+ </Button>
89
+ {open ? <div role="group" aria-label={label} className={cn("flex w-full flex-wrap items-center gap-2", className)}>{children}</div> : null}
90
+ </>
91
+ );
92
+ }
@@ -9,6 +9,7 @@
9
9
  export * from "./bar-list";
10
10
  export * from "./detail-panel";
11
11
  export * from "./entity-avatar";
12
+ export * from "./filter-controls";
12
13
  export * from "./form-sheet";
13
14
  export * from "./key-value-list";
14
15
  export * from "./screen-layout";
@@ -95,7 +95,7 @@ export function SplitView({ list, detail, detailOpen, onCloseDetail, detailWidth
95
95
 
96
96
  return (
97
97
  <div className={cn("flex h-full min-h-0 w-full min-w-0", className)} data-testid="split-view" data-detail-open={open ? "true" : "false"}>
98
- <div className={cn("min-h-0 min-w-0 flex-1", listClassName)}>{list}</div>
98
+ <div className={cn("min-h-0 min-w-0 flex-1", listClassName)} data-g8-list="">{list}</div>
99
99
  {open && !narrow ? (
100
100
  <aside
101
101
  aria-label={detailTitle}
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ /**
4
+ * useAllPages: read every page of a list that a screen needs IN FULL (a lookup by id, a dropdown that must offer every
5
+ * row), instead of one call with a hopeful `limit`. Pages are fetched one after the other until the API says there is no
6
+ * next page, or `maxPages` is reached; then `truncated` is true and the screen must say so (see FirstNotice).
7
+ *
8
+ * It calls no operation itself: `fetchPage` does, so the caller's component declares the operation.
9
+ */
10
+ import { useG8Query } from "./use-g8-query";
11
+ import type { RowsPage } from "../lib/paging";
12
+
13
+ export interface UseAllPagesOptions {
14
+ enabled?: boolean;
15
+ /** Stop after this many pages (default 20) so a runaway list cannot loop forever. */
16
+ maxPages?: number;
17
+ /** The page size you read with: when a page does not say `hasNext`, a full page of this size means "maybe more". */
18
+ pageSize?: number;
19
+ }
20
+
21
+ export interface UseAllPages<T> {
22
+ items: T[];
23
+ loading: boolean;
24
+ /** The first read has finished (a row list may still be empty). */
25
+ loaded: boolean;
26
+ error: unknown;
27
+ /** The page limit was reached with more rows still on the server: some rows are NOT in `items`. */
28
+ truncated: boolean;
29
+ refetch: () => void;
30
+ }
31
+
32
+ interface All<T> { items: T[]; truncated: boolean }
33
+
34
+ export function useAllPages<T>(
35
+ fetchPage: (page: number, signal: AbortSignal) => Promise<RowsPage<T>>,
36
+ deps: readonly unknown[],
37
+ options: UseAllPagesOptions = {},
38
+ ): UseAllPages<T> {
39
+ const max = options.maxPages ?? 20;
40
+ const q = useG8Query<All<T>>(async (signal) => {
41
+ const items: T[] = [];
42
+ for (let page = 1; page <= max; page++) {
43
+ const got = await fetchPage(page, signal);
44
+ items.push(...got.rows);
45
+ if (got.hasNext === false || got.rows.length === 0) return { items, truncated: false };
46
+ if (got.hasNext === undefined && (options.pageSize === undefined || got.rows.length < options.pageSize)) return { items, truncated: false }; // could not tell, and not a full page
47
+ }
48
+ return { items, truncated: true };
49
+ }, deps, { enabled: options.enabled });
50
+ return { items: q.data?.items ?? EMPTY, loading: q.loading, loaded: q.data !== undefined, error: q.error, truncated: q.data?.truncated ?? false, refetch: () => { void q.refetch(); } };
51
+ }
52
+
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ const EMPTY: any[] = [];
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ /**
4
+ * useLoadMoreList: a list read that never stops at its first page. It has the shape of a plain `useG8Query` list
5
+ * (`data`, `loading`, `error`, `refetch`) plus `hasMore` / `loadMore` / `loadingMore`, so a screen that read one capped
6
+ * page can adopt it by swapping the hook and rendering <LoadMore> under the rows.
7
+ *
8
+ * const list = useLoadMoreList(byOffset(100, async (offset, signal) => {
9
+ * const env = await g8Get("/landing-pages", { limit: 100, offset }, { signal });
10
+ * const rows = listOf(env.data);
11
+ * return { rows, hasNext: hasNextOf(env, 100, rows.length) };
12
+ * }), [status]);
13
+ * ...
14
+ * <LoadMore shown={rows.length} noun="landing pages" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
15
+ *
16
+ * Built on usePaginated (changing `deps` starts over, stale responses are dropped). It calls no operation itself.
17
+ */
18
+ import { useRef } from "react";
19
+ import { usePaginated } from "./use-paginated";
20
+ import type { UsePaginatedOptions } from "./use-paginated";
21
+ import type { PaginatedResponse } from "@graph8/sdk";
22
+
23
+ export interface UseLoadMoreList<T> {
24
+ /** The rows loaded so far; undefined until the first page arrives. */
25
+ data: T[] | undefined;
26
+ loading: boolean;
27
+ loadingMore: boolean;
28
+ error: unknown;
29
+ hasMore: boolean;
30
+ loadMore: () => void;
31
+ /** Start over from the first page; resolves with that first page of rows (what a poll needs to decide), undefined if it failed. */
32
+ refetch: () => Promise<T[] | undefined>;
33
+ }
34
+
35
+ export function useLoadMoreList<T>(
36
+ fetchPage: (cursor: string | undefined, signal: AbortSignal) => Promise<PaginatedResponse<T>>,
37
+ deps: readonly unknown[],
38
+ options: UsePaginatedOptions = {},
39
+ ): UseLoadMoreList<T> {
40
+ const first = useRef<T[] | undefined>(undefined);
41
+ const p = usePaginated<T>(async (cursor, signal) => {
42
+ const page = await fetchPage(cursor, signal);
43
+ if (cursor === undefined) first.current = [...(page.data ?? [])];
44
+ return page;
45
+ }, deps, options);
46
+ return {
47
+ data: p.loading && p.items.length === 0 ? undefined : p.items,
48
+ loading: p.loading,
49
+ loadingMore: p.loadingMore,
50
+ error: p.error,
51
+ hasMore: p.hasMore,
52
+ loadMore: () => { void p.loadMore(); },
53
+ // Resolves with the FIRST page of the new load (first.current is set by the wrapped fetcher above), not the whole list:
54
+ // enough for a poll that only asks "is anything still generating?".
55
+ refetch: async () => {
56
+ first.current = undefined;
57
+ await p.reload();
58
+ return first.current;
59
+ },
60
+ };
61
+ }