@graph8/sdk 0.362.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 (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordLink: a record's name as a real `<a href>`. A plain click runs `onOpen` (the screen opens the record
5
+ * the way it always did: its own URL state or component state); Cmd/Ctrl/Shift/Alt-click and middle-click fall
6
+ * through to the browser, so "open in a new tab" lands on the same screen with the record open. The href is
7
+ * `?<param>=<id>`, where `param` is the URL param the OWNING screen reads for this record type. Without a `param`
8
+ * there is no honest href (a new tab would open nothing, or the wrong record), so it renders a plain button that
9
+ * only runs `onOpen`. The href is relative (SSR-safe) and is completed with the current query when the pointer or
10
+ * focus reaches it, so Cmd-click keeps `tab`, `pipeline` and the rest. Add `full` for `view=full`. Only a screen
11
+ * without an `onOpen` of its own should pass `writeUrl`, which makes a plain click write the param itself.
12
+ *
13
+ * SDK operations: none.
14
+ */
15
+ import type { MouseEvent, ReactNode, SyntheticEvent } from "react";
16
+ import { cn } from "@graph8/sdk/ui";
17
+ import { writeUrlParam } from "../../hooks/use-url-state";
18
+
19
+ export interface RecordLinkProps {
20
+ /** The record id written to the URL param. */
21
+ id: string | number;
22
+ /** URL param the owning screen reads for this record. Omit it when the screen reads none: a button is rendered. */
23
+ param?: string;
24
+ /** Also set `view=full` (the record as a full page). */
25
+ full?: boolean;
26
+ /** Runs on a plain click: open the record the way the screen does (its URL setter or component state). */
27
+ onOpen?: () => void;
28
+ /** A plain click also writes `param` (and `view`) to the URL itself. Default false: `onOpen` owns the state. */
29
+ writeUrl?: boolean;
30
+ className?: string;
31
+ children: ReactNode;
32
+ }
33
+
34
+ /** Relative href that opens `id`: `?param=id[&view=full]`. Depends on nothing but its arguments, so server and client agree. */
35
+ export function recordHref(id: string | number, param: string, full = false): string {
36
+ return `?${param}=${encodeURIComponent(String(id))}${full ? "&view=full" : ""}`;
37
+ }
38
+
39
+ /** The same href merged into the page's current query (call it at event time, never during render). */
40
+ export function currentRecordHref(id: string | number, param: string, full = false): string {
41
+ const url = new URL(window.location.href);
42
+ url.searchParams.set(param, String(id));
43
+ if (full) url.searchParams.set("view", "full");
44
+ else url.searchParams.delete("view");
45
+ return `${url.pathname}${url.search}${url.hash}`;
46
+ }
47
+
48
+ export function RecordLink({ id, param, full = false, onOpen, writeUrl = false, className, children }: RecordLinkProps) {
49
+ const key = param ?? (writeUrl ? "id" : undefined);
50
+ const cls = cn("font-medium text-foreground underline-offset-2 hover:underline focus-visible:underline", className);
51
+ if (!key) {
52
+ return (
53
+ <button type="button" className={cn(cls, "text-left")} onClick={(e) => { e.stopPropagation(); onOpen?.(); }}>
54
+ {children}
55
+ </button>
56
+ );
57
+ }
58
+ const onClick = (e: MouseEvent<HTMLAnchorElement>): void => {
59
+ e.stopPropagation(); // the row around it also opens the record: do not do it twice
60
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
61
+ if (!onOpen && !writeUrl) return; // nothing to do in place: let the browser follow the href
62
+ e.preventDefault();
63
+ if (writeUrl) {
64
+ writeUrlParam("view", full ? "full" : null);
65
+ writeUrlParam(key, String(id), { push: true });
66
+ }
67
+ onOpen?.();
68
+ };
69
+ /** Complete the href with the current query just before the browser can use it (hover, press, focus, context menu). */
70
+ const complete = (e: SyntheticEvent<HTMLAnchorElement>): void => e.currentTarget.setAttribute("href", currentRecordHref(id, key, full));
71
+ return (
72
+ <a href={recordHref(id, key, full)} onClick={onClick} onMouseEnter={complete} onMouseDown={complete} onFocus={complete} onContextMenu={complete} onTouchStart={complete} className={cls}>
73
+ {children}
74
+ </a>
75
+ );
76
+ }
@@ -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,65 @@
1
+ "use client";
2
+
3
+ /**
4
+ * The recharts half of TimeSeriesChart, in its own module so it is code-split: `time-series-chart.tsx`
5
+ * loads it on demand (React.lazy), and pages without a chart never download recharts. Import this file
6
+ * directly only if you need the chart rendered synchronously (it then joins your page bundle).
7
+ *
8
+ * SDK operations: none. Peer: recharts.
9
+ */
10
+ import { useMemo } from "react";
11
+ import { Area, AreaChart, Bar, BarChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
12
+ import { ChartContainer, ChartTooltip, ChartTooltipContent, cn, type ChartConfig } from "@graph8/sdk/ui";
13
+ import { TONE_VAR, defaultFormatX, defaultFormatY } from "./time-series-format";
14
+ import type { TimeSeries, TimeSeriesChartProps } from "./time-series-format";
15
+
16
+ export function TimeSeriesChartRecharts({ data, xKey, series, kind = "line", height = 220, formatX = defaultFormatX, formatY = defaultFormatY, stacked, className }: TimeSeriesChartProps) {
17
+ const config = useMemo<ChartConfig>(
18
+ () => Object.fromEntries(series.map((s) => [s.key, { label: s.label ?? s.key, color: TONE_VAR[s.tone ?? "ink"] }])),
19
+ [series],
20
+ );
21
+ const axes = (
22
+ <>
23
+ <CartesianGrid vertical={false} strokeDasharray="0" stroke="var(--border)" strokeOpacity={0.9} />
24
+ <XAxis dataKey={xKey} tickLine={false} axisLine={false} tickMargin={8} minTickGap={24} tickFormatter={formatX} tick={{ fontSize: 11 }} />
25
+ <YAxis width={36} tickLine={false} axisLine={false} tickMargin={4} tickFormatter={formatY} tick={{ fontSize: 11 }} allowDecimals={false} />
26
+ <ChartTooltip cursor={{ stroke: "var(--border)" }} content={<ChartTooltipContent labelFormatter={(v) => formatX(v)} formatter={(v, name) => [`${formatY(Number(v))} `, String(config[String(name)]?.label ?? name)]} indicator="line" />} />
27
+ </>
28
+ );
29
+ const stroke = (s: TimeSeries) => ({
30
+ stroke: `var(--color-${s.key})`,
31
+ strokeWidth: s.tone === "prior" ? 1.25 : 1.5,
32
+ strokeDasharray: s.tone === "prior" ? "4 3" : undefined,
33
+ dot: false as const,
34
+ activeDot: { r: 3 },
35
+ isAnimationActive: false,
36
+ });
37
+ const style = { height } as const;
38
+
39
+ return (
40
+ <ChartContainer config={config} className={cn("aspect-auto w-full text-text-color-muted", className)} style={style} data-testid="time-series-chart">
41
+ {kind === "bar" ? (
42
+ <BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }} barCategoryGap="25%">
43
+ {axes}
44
+ {series.map((s) => (
45
+ <Bar key={s.key} dataKey={s.key} name={s.key} stackId={stacked ? "a" : undefined} fill={`var(--color-${s.key})`} fillOpacity={s.tone ? 0.85 : 1} radius={[2, 2, 0, 0]} isAnimationActive={false} />
46
+ ))}
47
+ </BarChart>
48
+ ) : kind === "area" ? (
49
+ <AreaChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
50
+ {axes}
51
+ {series.map((s) => (
52
+ <Area key={s.key} type="monotone" dataKey={s.key} name={s.key} stackId={stacked ? "a" : undefined} fill={`var(--color-${s.key})`} fillOpacity={s.tone === "prior" ? 0 : 0.06} {...stroke(s)} />
53
+ ))}
54
+ </AreaChart>
55
+ ) : (
56
+ <LineChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
57
+ {axes}
58
+ {series.map((s) => (
59
+ <Line key={s.key} type="monotone" dataKey={s.key} name={s.key} {...stroke(s)} />
60
+ ))}
61
+ </LineChart>
62
+ )}
63
+ </ChartContainer>
64
+ );
65
+ }
@@ -6,109 +6,27 @@
6
6
  * A `prior` series is the dashed warm-grey comparison. Chrome is recessive: hairline gridlines, muted
7
7
  * axis labels, no borders. Built on recharts through the UI kit's ChartContainer.
8
8
  *
9
+ * recharts is code-split: the chart module (time-series-chart-recharts.tsx) loads on first render, with a
10
+ * same-height skeleton meanwhile, so a page that shows no chart never downloads it. Same props as before.
11
+ *
9
12
  * SDK operations: none. Peer: recharts.
10
13
  */
11
- import { useMemo } from "react";
12
- import { Area, AreaChart, Bar, BarChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
13
- import { ChartContainer, ChartTooltip, ChartTooltipContent, cn, type ChartConfig } from "@graph8/sdk/ui";
14
- import type { Tone } from "./status-pill";
15
-
16
- export interface TimeSeries {
17
- /** The field of each row to plot. */
18
- key: string;
19
- label?: string;
20
- /** Ink unless the direction means good or bad. "prior" is the dashed comparison line. */
21
- tone?: Tone | "prior";
22
- }
23
-
24
- export interface TimeSeriesChartProps {
25
- data: Array<Record<string, unknown>>;
26
- /** Field holding the x value (an ISO date, a label). */
27
- xKey: string;
28
- series: TimeSeries[];
29
- kind?: "line" | "area" | "bar";
30
- height?: number;
31
- /** Format an x value for the axis ("2026-10-01" -> "Oct 1"). */
32
- formatX?: (value: unknown) => string;
33
- /** Format a y value for the axis and the tooltip. */
34
- formatY?: (value: number) => string;
35
- /** Stack bars/areas. */
36
- stacked?: boolean;
37
- className?: string;
38
- }
14
+ import { Suspense, lazy } from "react";
15
+ import { Skeleton, cn } from "@graph8/sdk/ui";
16
+ import type { TimeSeriesChartProps } from "./time-series-format";
39
17
 
40
- const TONE_VAR: Record<NonNullable<TimeSeries["tone"]> | "ink", string> = {
41
- ink: "var(--foreground)",
42
- neutral: "var(--text-color-muted)",
43
- info: "var(--info-foreground)",
44
- positive: "var(--positive-foreground)",
45
- warning: "var(--warning-foreground)",
46
- negative: "var(--error-foreground)",
47
- prior: "var(--text-color-muted)",
48
- };
18
+ export type { TimeSeries, TimeSeriesChartProps } from "./time-series-format";
19
+ export { defaultFormatX, defaultFormatY } from "./time-series-format";
49
20
 
50
- export function defaultFormatX(value: unknown): string {
51
- if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)) {
52
- const d = new Date(value.length === 10 ? `${value}T00:00:00Z` : value);
53
- if (!Number.isNaN(d.getTime())) return d.toLocaleDateString(undefined, { month: "short", day: "numeric", timeZone: value.length === 10 ? "UTC" : undefined });
54
- }
55
- return String(value ?? "");
56
- }
57
-
58
- export function defaultFormatY(value: number): string {
59
- if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
60
- if (Math.abs(value) >= 10_000) return `${(value / 1_000).toFixed(0)}k`;
61
- if (Math.abs(value) >= 1_000) return `${(value / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
62
- return value.toLocaleString();
63
- }
21
+ const Recharts = lazy(() => import("./time-series-chart-recharts").then((m) => ({ default: m.TimeSeriesChartRecharts })));
64
22
 
65
- export function TimeSeriesChart({ data, xKey, series, kind = "line", height = 220, formatX = defaultFormatX, formatY = defaultFormatY, stacked, className }: TimeSeriesChartProps) {
66
- const config = useMemo<ChartConfig>(
67
- () => Object.fromEntries(series.map((s) => [s.key, { label: s.label ?? s.key, color: TONE_VAR[s.tone ?? "ink"] }])),
68
- [series],
23
+ export function TimeSeriesChart(props: TimeSeriesChartProps) {
24
+ const fallback = (
25
+ <Skeleton className={cn("w-full", props.className)} style={{ height: props.height ?? 220 }} aria-label="Loading chart" data-testid="time-series-chart-loading" />
69
26
  );
70
- const axes = (
71
- <>
72
- <CartesianGrid vertical={false} strokeDasharray="0" stroke="var(--border)" strokeOpacity={0.9} />
73
- <XAxis dataKey={xKey} tickLine={false} axisLine={false} tickMargin={8} minTickGap={24} tickFormatter={formatX} tick={{ fontSize: 11 }} />
74
- <YAxis width={36} tickLine={false} axisLine={false} tickMargin={4} tickFormatter={formatY} tick={{ fontSize: 11 }} allowDecimals={false} />
75
- <ChartTooltip cursor={{ stroke: "var(--border)" }} content={<ChartTooltipContent labelFormatter={(v) => formatX(v)} formatter={(v, name) => [`${formatY(Number(v))} `, String(config[String(name)]?.label ?? name)]} indicator="line" />} />
76
- </>
77
- );
78
- const stroke = (s: TimeSeries) => ({
79
- stroke: `var(--color-${s.key})`,
80
- strokeWidth: s.tone === "prior" ? 1.25 : 1.5,
81
- strokeDasharray: s.tone === "prior" ? "4 3" : undefined,
82
- dot: false as const,
83
- activeDot: { r: 3 },
84
- isAnimationActive: false,
85
- });
86
- const style = { height } as const;
87
-
88
27
  return (
89
- <ChartContainer config={config} className={cn("aspect-auto w-full text-text-color-muted", className)} style={style} data-testid="time-series-chart">
90
- {kind === "bar" ? (
91
- <BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }} barCategoryGap="25%">
92
- {axes}
93
- {series.map((s) => (
94
- <Bar key={s.key} dataKey={s.key} name={s.key} stackId={stacked ? "a" : undefined} fill={`var(--color-${s.key})`} fillOpacity={s.tone ? 0.85 : 1} radius={[2, 2, 0, 0]} isAnimationActive={false} />
95
- ))}
96
- </BarChart>
97
- ) : kind === "area" ? (
98
- <AreaChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
99
- {axes}
100
- {series.map((s) => (
101
- <Area key={s.key} type="monotone" dataKey={s.key} name={s.key} stackId={stacked ? "a" : undefined} fill={`var(--color-${s.key})`} fillOpacity={s.tone === "prior" ? 0 : 0.06} {...stroke(s)} />
102
- ))}
103
- </AreaChart>
104
- ) : (
105
- <LineChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
106
- {axes}
107
- {series.map((s) => (
108
- <Line key={s.key} type="monotone" dataKey={s.key} name={s.key} {...stroke(s)} />
109
- ))}
110
- </LineChart>
111
- )}
112
- </ChartContainer>
28
+ <Suspense fallback={fallback}>
29
+ <Recharts {...props} />
30
+ </Suspense>
113
31
  );
114
32
  }
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Types and formatters shared by TimeSeriesChart and its lazily loaded recharts implementation.
5
+ * No recharts here, so importing them costs nothing.
6
+ */
7
+ import type { Tone } from "./status-pill";
8
+
9
+ export interface TimeSeries {
10
+ /** The field of each row to plot. */
11
+ key: string;
12
+ label?: string;
13
+ /** Ink unless the direction means good or bad. "prior" is the dashed comparison line. */
14
+ tone?: Tone | "prior";
15
+ }
16
+
17
+ export interface TimeSeriesChartProps {
18
+ data: Array<Record<string, unknown>>;
19
+ /** Field holding the x value (an ISO date, a label). */
20
+ xKey: string;
21
+ series: TimeSeries[];
22
+ kind?: "line" | "area" | "bar";
23
+ height?: number;
24
+ /** Format an x value for the axis ("2026-10-01" -> "Oct 1"). */
25
+ formatX?: (value: unknown) => string;
26
+ /** Format a y value for the axis and the tooltip. */
27
+ formatY?: (value: number) => string;
28
+ /** Stack bars/areas. */
29
+ stacked?: boolean;
30
+ className?: string;
31
+ }
32
+
33
+ export const TONE_VAR: Record<NonNullable<TimeSeries["tone"]> | "ink", string> = {
34
+ ink: "var(--foreground)",
35
+ neutral: "var(--text-color-muted)",
36
+ info: "var(--info-foreground)",
37
+ positive: "var(--positive-foreground)",
38
+ warning: "var(--warning-foreground)",
39
+ negative: "var(--error-foreground)",
40
+ prior: "var(--text-color-muted)",
41
+ };
42
+
43
+ export function defaultFormatX(value: unknown): string {
44
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)) {
45
+ const d = new Date(value.length === 10 ? `${value}T00:00:00Z` : value);
46
+ if (!Number.isNaN(d.getTime())) return d.toLocaleDateString(undefined, { month: "short", day: "numeric", timeZone: value.length === 10 ? "UTC" : undefined });
47
+ }
48
+ return String(value ?? "");
49
+ }
50
+
51
+ export function defaultFormatY(value: number): string {
52
+ if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
53
+ if (Math.abs(value) >= 10_000) return `${(value / 1_000).toFixed(0)}k`;
54
+ if (Math.abs(value) >= 1_000) return `${(value / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
55
+ return value.toLocaleString();
56
+ }
57
+
@@ -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,52 @@
1
+ "use client";
2
+
3
+ /**
4
+ * White-label: who the app is and what the platform behind it is called. Every block reads the platform's name
5
+ * and link-outs from here ("Connect in graph8", "Open in graph8", the settings pages they open), so an agency or
6
+ * reseller can show its own name and its own app URL everywhere, from one config (`g8.config.ts` in a created app,
7
+ * passed to <BrandProvider>).
8
+ *
9
+ * const brand = useBrand(); // { name, appUrl, linkLabel, logo? }
10
+ * brandUrl(brand, "/studio/settings?tab=integrations")
11
+ *
12
+ * With no provider, or for any field left out, the default is graph8 itself (app.graph8.com), so nothing changes
13
+ * for an app that does not brand.
14
+ */
15
+
16
+ import { createContext, useContext } from "react";
17
+
18
+ export interface BrandConfig {
19
+ /** The app's own name (sidebar, page title). */
20
+ name: string;
21
+ /** Base URL of the platform's web app; link-outs append their path. No trailing slash needed. */
22
+ appUrl: string;
23
+ /** What the platform is called in copy and links ("Connect in <linkLabel>"). */
24
+ linkLabel: string;
25
+ /** Public path or URL of the logo (`/brand/logo.svg`). */
26
+ logo?: string;
27
+ }
28
+
29
+ export const DEFAULT_BRAND: BrandConfig = { name: "graph8 app", appUrl: "https://app.graph8.com", linkLabel: "graph8" };
30
+
31
+ /** Fill the gaps of a partial config with the defaults; an appUrl that is not http(s) falls back to the default. */
32
+ export function resolveBrand(partial?: Partial<BrandConfig> | null): BrandConfig {
33
+ const p = partial ?? {};
34
+ const text = (v: unknown, d: string) => (typeof v === "string" && v.trim() ? v.trim() : d);
35
+ let appUrl = DEFAULT_BRAND.appUrl;
36
+ if (typeof p.appUrl === "string" && /^https?:\/\/[^\s/]+/i.test(p.appUrl.trim())) appUrl = p.appUrl.trim().replace(/\/+$/, "");
37
+ const logo = typeof p.logo === "string" && p.logo.trim() ? p.logo.trim() : undefined;
38
+ return { name: text(p.name, DEFAULT_BRAND.name), appUrl, linkLabel: text(p.linkLabel, DEFAULT_BRAND.linkLabel), ...(logo ? { logo } : {}) };
39
+ }
40
+
41
+ export const BrandContext = createContext<BrandConfig>(DEFAULT_BRAND);
42
+
43
+ /** The current brand: the nearest <BrandProvider>'s, else graph8's defaults. */
44
+ export function useBrand(): BrandConfig {
45
+ return useContext(BrandContext);
46
+ }
47
+
48
+ /** `appUrl` + a path on the platform (`"/lists"`, `"/studio/settings?tab=api"`); no path gives the app's home. */
49
+ export function brandUrl(brand: Pick<BrandConfig, "appUrl">, path = ""): string {
50
+ if (!path) return brand.appUrl;
51
+ return `${brand.appUrl}${path.startsWith("/") || path.startsWith("?") ? "" : "/"}${path}`;
52
+ }
@@ -1,10 +1,22 @@
1
1
  "use client";
2
2
 
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { g8QueryCache } from "../lib/g8-cache";
5
+ import type { G8QueryKey } from "../lib/g8-cache";
4
6
 
5
7
  export interface UseG8MutationOptions<TVars, TResult> {
6
8
  onSuccess?: (result: TResult, vars: TVars) => void;
7
9
  onError?: (error: unknown, vars: TVars) => void;
10
+ /**
11
+ * Cache keys (prefixes) to mark stale after a successful write, so every list and detail showing them
12
+ * refreshes: `invalidates: ["contacts"]`, or a function of the result: `(r, v) => [["contacts", "detail", v.id]]`.
13
+ */
14
+ invalidates?: readonly G8QueryKey[] | ((result: TResult, vars: TVars) => readonly G8QueryKey[]);
15
+ /**
16
+ * Optimistic update: change cached data under `prefix` before the request; on failure it is put back.
17
+ * Either way the prefix is invalidated afterwards so the server's answer wins.
18
+ */
19
+ optimistic?: { prefix: G8QueryKey; update: (old: any, vars: TVars, key: readonly unknown[]) => any }; // eslint-disable-line @typescript-eslint/no-explicit-any
8
20
  }
9
21
 
10
22
  export interface UseG8MutationResult<TVars, TResult> {
@@ -54,15 +66,26 @@ export function useG8Mutation<TVars, TResult>(
54
66
  const id = ++latest.current;
55
67
  setLoading(true);
56
68
  setError(undefined);
69
+ const { optimistic } = optionsRef.current;
70
+ const snapshot = optimistic
71
+ ? g8QueryCache.setQueriesData(optimistic.prefix, (old, key) => optimistic.update(old, vars, key))
72
+ : null;
57
73
  try {
58
74
  const result = await mutationRef.current(vars);
59
75
  if (id === latest.current && mounted.current) {
60
76
  setData(result);
61
77
  setLoading(false);
62
78
  }
79
+ const inv = optionsRef.current.invalidates;
80
+ const prefixes = typeof inv === "function" ? inv(result, vars) : inv ?? [];
81
+ for (const prefix of optimistic ? [...prefixes, optimistic.prefix] : prefixes) void g8QueryCache.invalidate(prefix);
63
82
  optionsRef.current.onSuccess?.(result, vars);
64
83
  return result;
65
84
  } catch (err) {
85
+ if (snapshot && optimistic) {
86
+ g8QueryCache.restore(snapshot);
87
+ void g8QueryCache.invalidate(optimistic.prefix);
88
+ }
66
89
  if (id === latest.current && mounted.current) {
67
90
  setError(err);
68
91
  setLoading(false);
@@ -1,12 +1,31 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { EMPTY_QUERY_STATE, g8QueryCache, hashKey } from "../lib/g8-cache";
5
+ import type { G8QueryKey, G8QueryObserver, G8QueryState } from "../lib/g8-cache";
4
6
 
5
7
  export interface UseG8QueryOptions<T> {
6
8
  /** Set false to hold the request (for example until a required id is known). */
7
9
  enabled?: boolean;
8
10
  /** Shown until the first response lands. */
9
11
  initialData?: T;
12
+ /**
13
+ * Opt in to the shared cache (lib/g8-cache.ts). Every component asking for the same key shares one
14
+ * request and one answer; cached data shows instantly and refreshes in the background. The key must
15
+ * name every input the fetcher reads: `["contacts", "detail", id]`. Without a key nothing changes.
16
+ */
17
+ key?: G8QueryKey;
18
+ /** With `key`: how long (ms) cached data counts as fresh, so a mount does not refetch it. Default 0. */
19
+ staleTime?: number;
20
+ /** With `key`: refresh when the window regains focus or the network returns (throttled). Default true. */
21
+ refetchOnFocus?: boolean;
22
+ /** With `key`: keep showing the previous key's data while a new key loads (like the uncached hook does). Default true. */
23
+ keepPrevious?: boolean;
24
+ /**
25
+ * With `key`: shown while the key has no data yet (for example the row a detail was opened from), with
26
+ * `isPlaceholderData: true`. NEVER seed an edit form from it: wait until `isPlaceholderData` is false.
27
+ */
28
+ placeholderData?: T;
10
29
  }
11
30
 
12
31
  export interface UseG8QueryResult<T> {
@@ -16,6 +35,12 @@ export interface UseG8QueryResult<T> {
16
35
  loading: boolean;
17
36
  /** Re-run the request now. Resolves with the data, or undefined if it failed or was superseded. */
18
37
  refetch: () => Promise<T | undefined>;
38
+ /** With `key` only: `data` is `placeholderData` (not the real record yet). */
39
+ isPlaceholderData?: boolean;
40
+ /** With `key` only: `data` belongs to the previous key while the new one loads. */
41
+ isPreviousData?: boolean;
42
+ /** With `key` only: epoch ms when the data was fetched (0 = not yet). */
43
+ updatedAt?: number;
19
44
  }
20
45
 
21
46
  /**
@@ -34,6 +59,19 @@ export function useG8Query<T>(
34
59
  fetcher: (signal: AbortSignal) => Promise<T>,
35
60
  deps: readonly unknown[],
36
61
  options: UseG8QueryOptions<T> = {},
62
+ ): UseG8QueryResult<T> {
63
+ // Both are always called (rules of hooks); only one is ever active.
64
+ const keyed = options.key !== undefined;
65
+ const cached = useCachedQuery(fetcher, deps, options, keyed);
66
+ const plain = usePlainQuery(fetcher, deps, keyed ? { ...options, enabled: false } : options);
67
+ return keyed ? cached : plain;
68
+ }
69
+
70
+ /** The uncached hook, exactly as it was before the cache existed. */
71
+ function usePlainQuery<T>(
72
+ fetcher: (signal: AbortSignal) => Promise<T>,
73
+ deps: readonly unknown[],
74
+ options: UseG8QueryOptions<T>,
37
75
  ): UseG8QueryResult<T> {
38
76
  const enabled = options.enabled ?? true;
39
77
  const fetcherRef = useRef(fetcher);
@@ -85,3 +123,91 @@ export function useG8Query<T>(
85
123
 
86
124
  return { data, error, loading, refetch: run };
87
125
  }
126
+
127
+ const noopSubscribe = (): (() => void) => () => undefined;
128
+ const emptySnapshot = (): G8QueryState => EMPTY_QUERY_STATE;
129
+
130
+ /** The keyed hook: state lives in the shared cache, this component only subscribes to it. */
131
+ function useCachedQuery<T>(
132
+ fetcher: (signal: AbortSignal) => Promise<T>,
133
+ deps: readonly unknown[],
134
+ options: UseG8QueryOptions<T>,
135
+ active: boolean,
136
+ ): UseG8QueryResult<T> {
137
+ const enabled = options.enabled ?? true;
138
+ const key = options.key ?? "";
139
+ const hash = active ? hashKey(key) : "";
140
+ const keyRef = useRef(key);
141
+ keyRef.current = key;
142
+ const fetcherRef = useRef(fetcher);
143
+ fetcherRef.current = fetcher;
144
+ const optionsRef = useRef(options);
145
+ optionsRef.current = options;
146
+
147
+ const observer = useRef<G8QueryObserver>({
148
+ fetcher: () => (signal) => fetcherRef.current(signal),
149
+ enabled: () => optionsRef.current.enabled ?? true,
150
+ staleTime: () => optionsRef.current.staleTime ?? 0,
151
+ refetchOnFocus: () => (optionsRef.current.enabled ?? true) && (optionsRef.current.refetchOnFocus ?? true),
152
+ }).current;
153
+
154
+ const subscribe = useCallback(
155
+ (onChange: () => void) => (active ? g8QueryCache.subscribe(keyRef.current, onChange, observer) : () => undefined),
156
+ // eslint-disable-next-line react-hooks/exhaustive-deps
157
+ [active, hash],
158
+ );
159
+ const getSnapshot = useCallback(
160
+ () => (active ? g8QueryCache.getState(keyRef.current) : EMPTY_QUERY_STATE),
161
+ // eslint-disable-next-line react-hooks/exhaustive-deps
162
+ [active, hash],
163
+ );
164
+ const state = useSyncExternalStore(active ? subscribe : noopSubscribe, active ? getSnapshot : emptySnapshot, emptySnapshot) as G8QueryState<T>;
165
+
166
+ // Same key, new deps: the fetcher changed under the key, so the cached answer may be wrong. Refetch.
167
+ const lastRun = useRef<{ hash: string; deps: readonly unknown[] } | null>(null);
168
+ useEffect(() => {
169
+ if (!active || !enabled) return;
170
+ const prev = lastRun.current;
171
+ const depsChanged =
172
+ prev !== null && prev.hash === hash && (prev.deps.length !== deps.length || prev.deps.some((d, i) => !Object.is(d, deps[i])));
173
+ lastRun.current = { hash, deps };
174
+ if (depsChanged) void g8QueryCache.fetch(keyRef.current, observer.fetcher(), { force: true });
175
+ else void g8QueryCache.ensureFresh(keyRef.current, observer.fetcher(), optionsRef.current.staleTime ?? 0);
176
+ // eslint-disable-next-line react-hooks/exhaustive-deps
177
+ }, [active, enabled, hash, ...deps]);
178
+
179
+ const previous = useRef<{ hash: string; data: T } | null>(null);
180
+ if (active && state.data !== undefined) previous.current = { hash, data: state.data };
181
+
182
+ const refetch = useCallback(
183
+ () =>
184
+ active
185
+ ? (g8QueryCache.fetch(keyRef.current, observer.fetcher(), { force: true, share: false }) as Promise<T | undefined>)
186
+ : Promise.resolve(undefined),
187
+ // eslint-disable-next-line react-hooks/exhaustive-deps
188
+ [active, hash],
189
+ );
190
+
191
+ let data = state.data;
192
+ let isPreviousData = false;
193
+ let isPlaceholderData = false;
194
+ if (data === undefined && (options.keepPrevious ?? true) && previous.current && previous.current.hash !== hash) {
195
+ data = previous.current.data;
196
+ isPreviousData = true;
197
+ }
198
+ if (data === undefined && options.placeholderData !== undefined) {
199
+ data = options.placeholderData;
200
+ isPlaceholderData = true;
201
+ }
202
+ if (data === undefined) data = options.initialData;
203
+ const neverLoaded = state.updatedAt === 0 && state.error === undefined;
204
+ return {
205
+ data,
206
+ error: state.error,
207
+ loading: enabled && (state.fetching || neverLoaded),
208
+ refetch,
209
+ isPlaceholderData,
210
+ isPreviousData,
211
+ updatedAt: state.updatedAt,
212
+ };
213
+ }