@graph8/sdk 0.361.0 → 0.370.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,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
+ }
@@ -2,9 +2,28 @@
2
2
 
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import type { PaginatedResponse } from "@graph8/sdk";
5
+ import { g8QueryCache } from "../lib/g8-cache";
6
+ import type { G8QueryKey } from "../lib/g8-cache";
7
+ import { useG8Query } from "./use-g8-query";
5
8
 
6
9
  export interface UsePaginatedOptions {
7
10
  enabled?: boolean;
11
+ /**
12
+ * Opt in to the shared cache: the loaded pages are kept under this key, shown instantly on the next
13
+ * mount and refreshed from the first page in the background; `invalidate(<prefix>)` reloads them.
14
+ * Name every filter in it: `["contacts", "cursor", { q }]`. Without a key nothing changes.
15
+ */
16
+ key?: G8QueryKey;
17
+ /** With `key`: ms the cached pages count as fresh. Default 0 (refresh on every mount). */
18
+ staleTime?: number;
19
+ /** With `key`: refresh (back to the first page) on window focus. Default false, so a long scroll is not lost. */
20
+ refetchOnFocus?: boolean;
21
+ }
22
+
23
+ /** What the cache holds for a keyed paginated list. */
24
+ export interface PaginatedCacheData<T> {
25
+ items: T[];
26
+ nextCursor?: string;
8
27
  }
9
28
 
10
29
  export interface UsePaginatedResult<T> {
@@ -35,6 +54,19 @@ export function usePaginated<T>(
35
54
  fetchPage: (cursor: string | undefined, signal: AbortSignal) => Promise<PaginatedResponse<T>>,
36
55
  deps: readonly unknown[],
37
56
  options: UsePaginatedOptions = {},
57
+ ): UsePaginatedResult<T> {
58
+ // Both are always called (rules of hooks); only one is ever active.
59
+ const keyed = options.key !== undefined;
60
+ const cached = useCachedPages(fetchPage, deps, options, keyed);
61
+ const plain = usePlainPages(fetchPage, deps, keyed ? { enabled: false } : options);
62
+ return keyed ? cached : plain;
63
+ }
64
+
65
+ /** The uncached hook, exactly as it was before the cache existed. */
66
+ function usePlainPages<T>(
67
+ fetchPage: (cursor: string | undefined, signal: AbortSignal) => Promise<PaginatedResponse<T>>,
68
+ deps: readonly unknown[],
69
+ options: UsePaginatedOptions,
38
70
  ): UsePaginatedResult<T> {
39
71
  const enabled = options.enabled ?? true;
40
72
  const fetchRef = useRef(fetchPage);
@@ -103,3 +135,91 @@ export function usePaginated<T>(
103
135
  const reload = useCallback(() => load(true), [load]);
104
136
  return { items, loading, loadingMore, error, hasMore, loadMore, reload };
105
137
  }
138
+
139
+ /** The keyed hook: the first page goes through the shared cache; "load more" appends to the cached pages. */
140
+ function useCachedPages<T>(
141
+ fetchPage: (cursor: string | undefined, signal: AbortSignal) => Promise<PaginatedResponse<T>>,
142
+ deps: readonly unknown[],
143
+ options: UsePaginatedOptions,
144
+ active: boolean,
145
+ ): UsePaginatedResult<T> {
146
+ const key = options.key ?? "";
147
+ const keyRef = useRef(key);
148
+ keyRef.current = key;
149
+ const fetchRef = useRef(fetchPage);
150
+ fetchRef.current = fetchPage;
151
+ const query = useG8Query<PaginatedCacheData<T>>(
152
+ async (signal) => {
153
+ const page = await fetchRef.current(undefined, signal);
154
+ return { items: [...(page.data ?? [])], nextCursor: page.pagination?.next_cursor ?? undefined };
155
+ },
156
+ deps,
157
+ {
158
+ key: active ? key : undefined,
159
+ enabled: active && (options.enabled ?? true),
160
+ staleTime: options.staleTime,
161
+ refetchOnFocus: options.refetchOnFocus ?? false,
162
+ },
163
+ );
164
+ const [loadingMore, setLoadingMore] = useState(false);
165
+ const [moreError, setMoreError] = useState<unknown>(undefined);
166
+ const more = useRef<AbortController | null>(null);
167
+
168
+ // A new key (filter) or unmount drops a "load more" in flight.
169
+ useEffect(() => {
170
+ return () => {
171
+ more.current?.abort();
172
+ more.current = null;
173
+ setLoadingMore(false);
174
+ };
175
+ // eslint-disable-next-line react-hooks/exhaustive-deps
176
+ }, [active ? JSON.stringify(key) : ""]);
177
+
178
+ const loadMore = useCallback(async (): Promise<void> => {
179
+ const start = g8QueryCache.getQueryData<PaginatedCacheData<T>>(keyRef.current);
180
+ if (!active || more.current || !start?.nextCursor) return;
181
+ const ctl = new AbortController();
182
+ more.current = ctl;
183
+ setLoadingMore(true);
184
+ try {
185
+ const page = await fetchRef.current(start.nextCursor, ctl.signal);
186
+ // The list was refreshed or replaced meanwhile: this page belongs to an older version.
187
+ if (ctl.signal.aborted || g8QueryCache.getQueryData(keyRef.current) !== start) return;
188
+ g8QueryCache.setQueryData<PaginatedCacheData<T>>(keyRef.current, {
189
+ items: [...start.items, ...(page.data ?? [])],
190
+ nextCursor: page.pagination?.next_cursor ?? undefined,
191
+ });
192
+ setMoreError(undefined);
193
+ } catch (err) {
194
+ if (!ctl.signal.aborted) setMoreError(err);
195
+ } finally {
196
+ if (more.current === ctl) {
197
+ more.current = null;
198
+ setLoadingMore(false);
199
+ }
200
+ }
201
+ }, [active]);
202
+
203
+ const reload = useCallback(async (): Promise<void> => {
204
+ more.current?.abort();
205
+ more.current = null;
206
+ setLoadingMore(false);
207
+ setMoreError(undefined);
208
+ await query.refetch();
209
+ }, [query.refetch]); // eslint-disable-line react-hooks/exhaustive-deps
210
+
211
+ const data = query.data;
212
+ return {
213
+ items: data?.items ?? EMPTY_ITEMS,
214
+ // Cached pages stay on screen while they refresh: only a list with nothing of its own yet is "loading".
215
+ loading: query.loading && (data === undefined || query.isPreviousData === true),
216
+ loadingMore,
217
+ error: query.error ?? moreError,
218
+ hasMore: Boolean(data?.nextCursor),
219
+ loadMore,
220
+ reload,
221
+ };
222
+ }
223
+
224
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
225
+ const EMPTY_ITEMS: any[] = [];
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ /**
4
+ * useRowActivation: make a whole list row open its record from the keyboard as well as the mouse.
5
+ * Spread the returned props on the row (`<TableRow {...rowProps}>` or a `<li>`): it becomes focusable,
6
+ * Enter and Space open it, and a click opens it, but a click or key on something INSIDE the row that
7
+ * has its own job (a checkbox, a button, a link, an input) is left alone.
8
+ *
9
+ * A table row keeps its `row` role (pass `role: undefined`, the default); pass `role: "button"` for a
10
+ * plain `<div>` or `<li>` row that is not part of a table or list.
11
+ *
12
+ * SDK operations: none.
13
+ */
14
+ import type { KeyboardEvent, MouseEvent } from "react";
15
+
16
+ const INTERACTIVE = "a,button,input,select,textarea,label,[role='button'],[role='checkbox'],[role='link'],[role='menuitem'],[data-row-ignore]";
17
+
18
+ export interface RowActivationProps {
19
+ role?: "button";
20
+ tabIndex: 0;
21
+ onClick: (e: MouseEvent<HTMLElement>) => void;
22
+ onKeyDown: (e: KeyboardEvent<HTMLElement>) => void;
23
+ }
24
+
25
+ /** True when the event started on a nested control that should keep its own behaviour. */
26
+ function onNestedControl(e: { target: EventTarget; currentTarget: EventTarget }): boolean {
27
+ const el = e.target as HTMLElement | null;
28
+ if (!el || el === (e.currentTarget as HTMLElement) || typeof el.closest !== "function") return false;
29
+ const hit = el.closest(INTERACTIVE);
30
+ return hit != null && hit !== (e.currentTarget as HTMLElement) && (e.currentTarget as HTMLElement).contains(hit);
31
+ }
32
+
33
+ /**
34
+ * The props, as a plain function: use it inside `.map()` callbacks (`<TableRow {...rowActivationProps(() => open(row.id))}>`),
35
+ * where a hook cannot be called. `useRowActivation` is the same thing for a component's own top level.
36
+ */
37
+ export function rowActivationProps(onOpen: () => void, options: { role?: "button" } = {}): RowActivationProps {
38
+ return {
39
+ ...(options.role ? { role: options.role } : {}),
40
+ tabIndex: 0,
41
+ onClick: (e) => {
42
+ if (onNestedControl(e)) return;
43
+ onOpen();
44
+ },
45
+ onKeyDown: (e) => {
46
+ if (e.target !== e.currentTarget) return; // Enter on an inner button is that button's own
47
+ if (e.key === "Enter" || e.key === " ") {
48
+ e.preventDefault();
49
+ onOpen();
50
+ }
51
+ },
52
+ };
53
+ }
54
+
55
+ export function useRowActivation(onOpen: () => void, options: { role?: "button" } = {}): RowActivationProps {
56
+ return rowActivationProps(onOpen, options);
57
+ }
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ /**
4
+ * useUrlState: one string in the URL query (`?id=123`, `?tab=activity`, `?q=ada`) as React state.
5
+ * The URL owns it, so the open record, the tab and the search survive reload, Back and sharing.
6
+ * Framework-agnostic: window.history.replaceState (pushState with `{ push: true }`) plus `popstate`,
7
+ * no next/navigation. SSR-safe: the server and the first client render see `defaultValue`.
8
+ *
9
+ * SDK operations: none.
10
+ */
11
+ import { useCallback, useSyncExternalStore } from "react";
12
+
13
+ const EVENT = "g8:urlstate";
14
+
15
+ type Listener = () => void;
16
+
17
+ function subscribe(listener: Listener): () => void {
18
+ if (typeof window !== "undefined") {
19
+ window.addEventListener("popstate", listener);
20
+ window.addEventListener(EVENT, listener);
21
+ }
22
+ return () => {
23
+ if (typeof window !== "undefined") {
24
+ window.removeEventListener("popstate", listener);
25
+ window.removeEventListener(EVENT, listener);
26
+ }
27
+ };
28
+ }
29
+
30
+ /** Current value of `key` in the page URL, or undefined. */
31
+ export function readUrlParam(key: string): string | undefined {
32
+ if (typeof window === "undefined") return undefined;
33
+ const v = new URLSearchParams(window.location.search).get(key);
34
+ return v == null ? undefined : v;
35
+ }
36
+
37
+ export interface SetUrlStateOptions {
38
+ /** Add a history entry (Back returns to the previous value) instead of replacing. */
39
+ push?: boolean;
40
+ }
41
+
42
+ /** Write `key` into the page URL (null, undefined or "" removes it) and notify every hook. */
43
+ export function writeUrlParam(key: string, value: string | null | undefined, options: SetUrlStateOptions = {}): void {
44
+ if (typeof window === "undefined") return;
45
+ const url = new URL(window.location.href);
46
+ const current = url.searchParams.get(key);
47
+ const next = value == null || value === "" ? null : String(value);
48
+ if (current === next) return;
49
+ if (next == null) url.searchParams.delete(key);
50
+ else url.searchParams.set(key, next);
51
+ const href = `${url.pathname}${url.search}${url.hash}`;
52
+ if (options.push) window.history.pushState(window.history.state, "", href);
53
+ else window.history.replaceState(window.history.state, "", href);
54
+ window.dispatchEvent(new Event(EVENT));
55
+ }
56
+
57
+ export type UrlStateSetter = (value: string | null | undefined, options?: SetUrlStateOptions) => void;
58
+
59
+ /**
60
+ * `const [id, setId] = useUrlState("id")`: `id` is the param or `defaultValue` ("" by default); `setId(null)`
61
+ * or `setId("")` removes it; `setId("42", { push: true })` adds a history entry.
62
+ */
63
+ export function useUrlState(key: string, defaultValue = ""): [string, UrlStateSetter] {
64
+ const value = useSyncExternalStore(
65
+ subscribe,
66
+ () => readUrlParam(key) ?? defaultValue,
67
+ () => defaultValue,
68
+ );
69
+ const set = useCallback<UrlStateSetter>(
70
+ (next, options) => writeUrlParam(key, next == null || next === defaultValue ? null : next, options),
71
+ [key, defaultValue],
72
+ );
73
+ return [value, set];
74
+ }