@graph8/sdk 0.362.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 (349) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +64 -1
  3. package/blocks/_foundation/block.json +79 -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/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/screen/index.ts +1 -0
  9. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  10. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  11. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  12. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  13. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  14. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  15. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  16. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  17. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  18. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  19. package/blocks/_foundation/lib/g8-client.ts +1 -1
  20. package/blocks/_foundation/lib/plural.ts +23 -0
  21. package/blocks/_foundation/server/connect-route.ts +15 -0
  22. package/blocks/_foundation/server/connect.ts +127 -0
  23. package/blocks/_foundation/server/local-key.ts +70 -0
  24. package/blocks/_foundation/server/proxy.ts +103 -3
  25. package/blocks/_foundation/server/route.ts +16 -4
  26. package/blocks/agents/README.md +14 -0
  27. package/blocks/agents/block.json +33 -1
  28. package/blocks/agents/components/agent-builder.tsx +13 -8
  29. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  30. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  31. package/blocks/agents/components/agent-runs.tsx +16 -7
  32. package/blocks/agents/components/agents-screen.tsx +184 -0
  33. package/blocks/analytics/README.md +13 -0
  34. package/blocks/analytics/block.json +45 -1
  35. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  36. package/blocks/analytics/components/leaderboard.tsx +14 -7
  37. package/blocks/analytics/components/report-builder.tsx +23 -9
  38. package/blocks/analytics/components/report-detail.tsx +84 -0
  39. package/blocks/analytics/components/trend-chart.tsx +62 -0
  40. package/blocks/appointments/block.json +6 -2
  41. package/blocks/appointments/components/availability-editor.tsx +17 -5
  42. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  43. package/blocks/appointments/components/event-type-related.tsx +4 -2
  44. package/blocks/appointments/components/meeting-list.tsx +9 -5
  45. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  46. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  47. package/blocks/block.schema.json +18 -0
  48. package/blocks/block.types.ts +17 -0
  49. package/blocks/campaigns/README.md +41 -11
  50. package/blocks/campaigns/block.json +105 -42
  51. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  52. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  53. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  54. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  55. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  56. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  57. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  58. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  59. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  60. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  61. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  62. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  63. package/blocks/coach/README.md +10 -0
  64. package/blocks/coach/block.json +16 -1
  65. package/blocks/coach/components/coach-screen.tsx +222 -0
  66. package/blocks/copilot/README.md +13 -0
  67. package/blocks/copilot/block.json +14 -1
  68. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  69. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  70. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  71. package/blocks/crm/README.md +28 -0
  72. package/blocks/crm/block.json +58 -2
  73. package/blocks/crm/components/companies-screen.tsx +345 -0
  74. package/blocks/crm/components/company-detail.tsx +8 -6
  75. package/blocks/crm/components/company-table.tsx +11 -4
  76. package/blocks/crm/components/contact-detail.tsx +6 -3
  77. package/blocks/crm/components/contact-table.tsx +10 -3
  78. package/blocks/crm/components/contacts-screen.tsx +359 -0
  79. package/blocks/crm/components/dedupe-review.tsx +7 -5
  80. package/blocks/crm/components/import-wizard.tsx +4 -2
  81. package/blocks/crm/components/notes-panel.tsx +4 -4
  82. package/blocks/crm/components/record-form.tsx +5 -3
  83. package/blocks/crm/components/task-list.tsx +4 -4
  84. package/blocks/crm/components/tasks-screen.tsx +379 -0
  85. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  86. package/blocks/crm/hooks/use-page-query.ts +7 -1
  87. package/blocks/crm/lib/crm-types.ts +12 -2
  88. package/blocks/crm/lib/screen-parts.tsx +105 -0
  89. package/blocks/deals/block.json +9 -3
  90. package/blocks/deals/components/deal-detail.tsx +23 -11
  91. package/blocks/deals/components/deal-related.tsx +1 -1
  92. package/blocks/deals/components/deals-screen.tsx +10 -5
  93. package/blocks/deals/components/stage-editor.tsx +4 -4
  94. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  95. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  96. package/blocks/dialer/README.md +34 -2
  97. package/blocks/dialer/block.json +67 -1
  98. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  99. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  100. package/blocks/dialer/components/call-log.tsx +7 -3
  101. package/blocks/dialer/components/call-queue.tsx +1 -1
  102. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  103. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  104. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  105. package/blocks/dialer/components/session-start-form.tsx +4 -2
  106. package/blocks/dialer/components/softphone.tsx +3 -1
  107. package/blocks/enrichment/README.md +14 -2
  108. package/blocks/enrichment/block.json +32 -4
  109. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  110. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  111. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  112. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  113. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  114. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  115. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  116. package/blocks/forms/block.json +32 -18
  117. package/blocks/forms/components/form-builder.tsx +3 -1
  118. package/blocks/forms/components/form-embed.tsx +3 -1
  119. package/blocks/forms/components/forms-screen.tsx +5 -2
  120. package/blocks/forms/components/submissions-table.tsx +3 -1
  121. package/blocks/inbox/README.md +27 -3
  122. package/blocks/inbox/block.json +14 -2
  123. package/blocks/inbox/components/inbox-list.tsx +7 -3
  124. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  125. package/blocks/inbox/components/reply-composer.tsx +65 -9
  126. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  127. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  128. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  129. package/blocks/inbox/lib/inbox-types.ts +47 -1
  130. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  131. package/blocks/integrations/README.md +9 -0
  132. package/blocks/integrations/block.json +46 -1
  133. package/blocks/integrations/components/crm-detail.tsx +100 -0
  134. package/blocks/integrations/components/integration-list.tsx +9 -4
  135. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  136. package/blocks/integrations/components/sync-status.tsx +5 -3
  137. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  138. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  139. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  140. package/blocks/intent/README.md +14 -0
  141. package/blocks/intent/block.json +27 -1
  142. package/blocks/intent/components/intent-feed.tsx +5 -2
  143. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  144. package/blocks/intent/components/intent-screen.tsx +265 -0
  145. package/blocks/intent/components/keyword-detail.tsx +124 -0
  146. package/blocks/lifecycle/block.json +13 -8
  147. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  148. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  149. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  150. package/blocks/lists/README.md +13 -0
  151. package/blocks/lists/block.json +39 -1
  152. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  153. package/blocks/lists/components/list-picker.tsx +5 -1
  154. package/blocks/lists/components/list-table.tsx +11 -5
  155. package/blocks/lists/components/lists-screen.tsx +344 -0
  156. package/blocks/lists/components/suppression-manager.tsx +6 -2
  157. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  158. package/blocks/mailboxes/README.md +17 -0
  159. package/blocks/mailboxes/block.json +65 -17
  160. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  161. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  162. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  163. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  164. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  165. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  166. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  167. package/blocks/newsletter/block.json +40 -24
  168. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  169. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  170. package/blocks/nurture/block.json +28 -14
  171. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  172. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  173. package/blocks/nurture/components/nurture-form.tsx +23 -3
  174. package/blocks/nurture/components/nurture-screen.tsx +10 -6
  175. package/blocks/nurture/lib/nurture-types.ts +1 -1
  176. package/blocks/objects/README.md +17 -0
  177. package/blocks/objects/block.json +29 -16
  178. package/blocks/objects/components/object-not-found.tsx +3 -1
  179. package/blocks/objects/components/object-record-detail.tsx +2 -1
  180. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  181. package/blocks/objects/components/object-table.tsx +13 -3
  182. package/blocks/objects/components/objects-screen.tsx +147 -0
  183. package/blocks/objects/lib/object-types.ts +1 -1
  184. package/blocks/pages/block.json +28 -23
  185. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  186. package/blocks/pages/components/content-library.tsx +9 -4
  187. package/blocks/pages/components/page-list.tsx +2 -1
  188. package/blocks/quotes/block.json +17 -12
  189. package/blocks/quotes/components/product-picker.tsx +3 -1
  190. package/blocks/quotes/components/quote-related.tsx +5 -2
  191. package/blocks/radar/README.md +10 -0
  192. package/blocks/radar/block.json +41 -1
  193. package/blocks/radar/components/competitor-detail.tsx +117 -0
  194. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  195. package/blocks/radar/components/radar-report.tsx +4 -2
  196. package/blocks/radar/components/radar-screen.tsx +214 -0
  197. package/blocks/radar/components/report-detail.tsx +104 -0
  198. package/blocks/search/README.md +10 -0
  199. package/blocks/search/block.json +16 -1
  200. package/blocks/search/components/filter-builder.tsx +3 -3
  201. package/blocks/search/components/prospect-results.tsx +5 -3
  202. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  203. package/blocks/search/components/saved-searches.tsx +4 -1
  204. package/blocks/search/hooks/use-filter-options.ts +20 -6
  205. package/blocks/search/lib/search-types.ts +2 -0
  206. package/blocks/sequencer/README.md +49 -9
  207. package/blocks/sequencer/block.json +99 -19
  208. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  209. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  210. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  211. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  212. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  213. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  214. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  215. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  216. package/blocks/sequencer/components/step-editor.tsx +3 -1
  217. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  218. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  219. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  220. package/blocks/social-ads/README.md +11 -0
  221. package/blocks/social-ads/block.json +101 -2
  222. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  223. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  224. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  225. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  226. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  227. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  228. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  229. package/blocks/social-ads/components/post-composer.tsx +3 -1
  230. package/blocks/social-ads/components/post-detail.tsx +160 -0
  231. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  232. package/blocks/social-ads/components/social-screen.tsx +160 -0
  233. package/blocks/visitors/README.md +10 -0
  234. package/blocks/visitors/block.json +25 -1
  235. package/blocks/visitors/components/snippet-install.tsx +3 -1
  236. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  237. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  238. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  239. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  240. package/blocks/webchat/README.md +10 -0
  241. package/blocks/webchat/block.json +21 -7
  242. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  243. package/blocks/webchat/components/conversation-list.tsx +7 -3
  244. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  245. package/blocks/workbench/README.md +11 -0
  246. package/blocks/workbench/block.json +48 -1
  247. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  248. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  249. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  250. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  251. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  252. package/blocks/workflows/README.md +13 -0
  253. package/blocks/workflows/block.json +28 -9
  254. package/blocks/workflows/components/run-history.tsx +31 -9
  255. package/blocks/workflows/components/trigger-config.tsx +3 -1
  256. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  257. package/blocks/workflows/components/workflow-list.tsx +13 -6
  258. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  259. package/dist/cli.js +2709 -1721
  260. package/dist/errors.d.mts +34 -0
  261. package/dist/errors.d.ts +34 -0
  262. package/dist/errors.js +46 -0
  263. package/dist/errors.js.map +1 -0
  264. package/dist/errors.mjs +21 -0
  265. package/dist/errors.mjs.map +1 -0
  266. package/dist/index.browser.js +11 -5
  267. package/dist/index.browser.js.map +1 -1
  268. package/dist/index.browser.mjs +11 -5
  269. package/dist/index.browser.mjs.map +1 -1
  270. package/dist/index.d.mts +164 -25
  271. package/dist/index.d.ts +164 -25
  272. package/dist/index.js +11 -5
  273. package/dist/index.js.map +1 -1
  274. package/dist/index.mjs +11 -5
  275. package/dist/index.mjs.map +1 -1
  276. package/dist/react.browser.js +10 -4
  277. package/dist/react.browser.js.map +1 -1
  278. package/dist/react.browser.mjs +10 -4
  279. package/dist/react.browser.mjs.map +1 -1
  280. package/dist/react.d.mts +142 -5
  281. package/dist/react.d.ts +142 -5
  282. package/dist/react.js +10 -4
  283. package/dist/react.js.map +1 -1
  284. package/dist/react.mjs +10 -4
  285. package/dist/react.mjs.map +1 -1
  286. package/dist/types/blocks/add.d.ts +11 -4
  287. package/dist/types/blocks/brand.d.ts +113 -0
  288. package/dist/types/blocks/compose.d.ts +53 -0
  289. package/dist/types/blocks/create.d.ts +22 -19
  290. package/dist/types/blocks/doctor.d.ts +5 -1
  291. package/dist/types/blocks/navfile.d.ts +35 -0
  292. package/dist/types/blocks/paths.d.ts +4 -0
  293. package/dist/types/blocks/prefixes.d.ts +37 -0
  294. package/dist/types/blocks/registry.d.ts +17 -0
  295. package/dist/types/blocks/screens.d.ts +53 -0
  296. package/dist/types/cli.d.ts +2 -0
  297. package/dist/types/errors.d.ts +33 -0
  298. package/dist/types/http.d.ts +2 -18
  299. package/dist/types/version.d.ts +2 -2
  300. package/docs/agents.md +15 -4
  301. package/docs/api/README.md +2 -2
  302. package/docs/api/by-path.md +1 -0
  303. package/docs/api/desk.d.ts +98 -2
  304. package/docs/api/desk.md +123 -1
  305. package/docs/api/inbox.d.ts +10 -2
  306. package/docs/api/inbox.md +10 -2
  307. package/docs/api/seo.d.ts +19 -1
  308. package/docs/api/seo.md +24 -1
  309. package/package.json +16 -2
  310. package/stacks/6sense.json +41 -16
  311. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  312. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  313. package/stacks/apollo.json +118 -36
  314. package/stacks/calendly.json +60 -0
  315. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  316. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  317. package/stacks/clay.json +59 -26
  318. package/stacks/gong.json +81 -35
  319. package/stacks/hootsuite.json +99 -0
  320. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  321. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  322. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  323. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  324. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  325. package/stacks/hubspot.json +167 -66
  326. package/stacks/intercom.json +48 -20
  327. package/stacks/mailchimp.json +52 -33
  328. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  329. package/stacks/outreach.json +88 -25
  330. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  331. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  332. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  333. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  334. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  335. package/stacks/pipedrive.json +115 -0
  336. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  337. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  338. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  339. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  340. package/stacks/salesforce.json +81 -31
  341. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  342. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  343. package/stacks/zoominfo.json +102 -0
  344. package/styles/styles.css +1 -1
  345. package/templates/next-app/README.md.tmpl +40 -9
  346. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  347. package/templates/next-app/components/app-shell.tsx.tmpl +99 -89
  348. package/templates/next-app/dot-gitignore.tmpl +2 -0
  349. package/templates/next-app/package.json.tmpl +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@ The most comprehensive GTM SDK. One `npm install`. Every graph8 capability.
8
8
  > operation in [`docs/api/`](docs/api/README.md). It ships with the package, so it matches this version.
9
9
  > Run `npx @graph8/sdk init-agent` to point Claude Code, Codex and Cursor in your project at it automatically.
10
10
  >
11
- > Blocks (copy-in components and hooks per feature): `npx @graph8/sdk list`, `search <text>`, `add <block>[/<Component>]`, `create <stack> <dir>` (a whole Next.js app: `npx @graph8/sdk create starter my-app`), and `doctor` to check your key, scopes and subscription. Each takes `--help`.
11
+ > Blocks (copy-in components and hooks per feature): `npx @graph8/sdk list`, `search <text>`, `add <block>[/<Component>]` (`--routes` also wires its screens as routes and nav entries in a created app), `create <stack> <dir>` (a whole Next.js app: `npx @graph8/sdk create starter my-app`), and `doctor` to check your key, scopes and subscription. Each takes `--help`.
12
12
 
13
13
  ## Install
14
14
 
@@ -496,15 +496,19 @@ npx @graph8/sdk create mailchimp my-app # then: cd my-app && cp .env.local.exa
496
496
  | Stack | Screens |
497
497
  |---|---|
498
498
  | `starter` | An empty app: proxy, shell and one landing page |
499
- | `mailchimp` | Contacts, companies, forms, submissions, lists, suppressions, newsletter, subscribers, nurture |
500
- | `clay` | Contacts, companies, lists, suppressions, prospect search, bulk enrichment, workbench |
501
- | `6sense` | Contacts, companies, intent feed, website visitors, competitor tracker |
502
- | `intercom` | Contacts, companies, web chat conversations, mailboxes, AI inbox, AI agents |
503
- | `outreach` | Contacts, companies, lists, mailboxes, sequences, inbox, dialer, call log, meetings, analytics |
504
- | `apollo` | Outreach's screens plus prospect search, enrichment, intent, visitors, deals and integrations |
505
- | `gong` | Contacts, companies, lists, dialer, call log, meetings, coaching feed, analytics |
506
- | `salesforce` | Contacts, companies, custom objects, lists, lifecycle, deals, quotes, analytics, workflows, copilot, integrations |
507
- | `hubspot` | The full CRM and marketing suite: forms, newsletter, nurture, web chat, sequences, inbox, pages, lifecycle, deals, quotes, workflows |
499
+ | `mailchimp` | Newsletter, nurture, forms, contacts, companies, tasks, lists, suppressions |
500
+ | `clay` | Prospect search, workbench, enrichment, lists, contacts, companies, tasks, suppressions |
501
+ | `6sense` | Intent, visitors, competitors, companies, contacts, tasks |
502
+ | `intercom` | Inbox, web chat conversations, contacts, companies, tasks, AI agents, mailboxes |
503
+ | `calendly` | Meetings and scheduling (event types, availability, booking pages) over contacts, companies and tasks |
504
+ | `pipedrive` | Analytics, deals, quotes, contacts, companies, inbox, meetings, scheduling, tasks and mailboxes; deals and quotes open each other, replies can start a deal |
505
+ | `outreach` | Analytics, contacts, companies, lists, sequences, campaigns, inbox, dialer, call log, meetings, scheduling, tasks, mailboxes, suppressions |
506
+ | `apollo` | Outreach's screens plus prospect search, enrichment, intent, visitors, deals and integrations; contacts get Enrich, Add to sequence and a Deals tab |
507
+ | `gong` | Analytics, call log, coaching, dialer, meetings, scheduling, contacts, companies, tasks, lists, suppressions |
508
+ | `salesforce` | Analytics, contacts, companies, lists, deals, quotes, lifecycle, tasks, workflows, copilot, custom objects, suppressions, integrations |
509
+ | `hubspot` | The full CRM and marketing suite: inbox, web chat, sequences, campaigns, newsletter, nurture, forms, pages, social, ads, deals, quotes, lifecycle, workflows |
510
+ | `zoominfo` | Prospect search, enrichment, intent and visitors over contacts, companies and lists |
511
+ | `hootsuite` | Social posts, ads, landing pages and analytics over your contacts and lists |
508
512
 
509
513
  Every available stack is proven by `npm run build && npm run e2e:create -- <stack>`, which packs the SDK, scaffolds the app with the built CLI, installs it and requires `next build` to pass. More stacks appear here as their blocks land; `create` refuses a stack that is not available yet.
510
514
 
@@ -8,7 +8,9 @@ Shared base every other block depends on. Install it first (the CLI does it for
8
8
  |---|---|
9
9
  | `lib/g8-client.ts` | Browser caller: `g8Get/g8Post/g8Put/g8Patch/g8Delete` -> your own `/api/g8/...` proxy. No API key. Returns the API envelope `{ data, pagination? }`, throws `G8Error`. |
10
10
  | `server/proxy.ts` | `createG8Proxy(config)`: the logic of the proxy (forwarding, allowlist, `authorize` hook). |
11
- | `server/route.ts` | Installs as `app/api/g8/[...path]/route.ts` and mounts the proxy. |
11
+ | `server/route.ts` | Installs as `app/api/g8/[...path]/route.ts` and mounts the proxy. `allowedPrefixes` is generated from the installed blocks' operations; local preview is on. |
12
+ | `server/local-key.ts`, `server/connect.ts`, `server/connect-route.ts` | Local preview key store: `app/api/g8-connect/route.ts` checks a pasted key (GET /me) and keeps it in `.g8/local-key` on the server. 404 in production. |
13
+ | `components/connect-graph8.tsx` | `ConnectGraph8Panel` + `G8ConnectGate`: the first-run Connect graph8 panel in `next dev`; a no-op in production. |
12
14
  | `hooks/use-g8-query.ts` | `useG8Query(fetcher, deps)` -> `{ data, error, loading, refetch }`. Latest request wins. |
13
15
  | `hooks/use-g8-mutation.ts` | `useG8Mutation(fn)` -> `{ mutate, mutateAsync, data, error, loading, reset }`. |
14
16
  | `hooks/use-paginated.ts` | `usePaginated(fetchPage, deps)` -> `{ items, hasMore, loadMore, reload, loading, loadingMore, error }`. Follows `pagination.next_cursor`. |
@@ -18,6 +20,7 @@ Shared base every other block depends on. Install it first (the CLI does it for
18
20
  | `components/empty-state.tsx` | `EmptyState`: empty list with a first step. |
19
21
  | `components/screen/*.tsx` | The **screen kit** (below): `ScreenLayout`, `StatStrip`, `SplitView`, `DetailPanel`, `Toolbar`, `StatusPill`, `EntityAvatar`, `SectionCard`, `FormSheet`, `KeyValueList`, `Sparkline`, `BarList`, `TimeSeriesChart`. Barrel: `@/g8/components/screen`. |
20
22
  | `hooks/use-url-state.ts` | `useUrlState(key, default = "")` -> `[value, set]`. One string in the URL query as state (replaceState + popstate, SSR-safe). |
23
+ | `lib/g8-cache.ts` | The shared query cache behind the hooks' optional `key`: dedupe, stale-while-revalidate, focus/reconnect refresh, `invalidate(prefix)`, `setQueryData`, bounded LRU. See "Query cache" below. |
21
24
 
22
25
  ## Flow of every call
23
26
 
@@ -30,6 +33,48 @@ Shared base every other block depends on. Install it first (the CLI does it for
30
33
  Ids that pass between calls always come from a previous response (a list's `id` into the next call's
31
34
  path). Never invent one.
32
35
 
36
+ ## Query cache (opt in with a `key`)
37
+
38
+ Every hook works exactly as before without a key. Pass one and the data lives in one app-wide cache
39
+ (`lib/g8-cache.ts`, no dependencies):
40
+
41
+ ```ts
42
+ const contacts = useG8Query(
43
+ (signal) => g8Get("/contacts", { page, limit: 25, q }, { signal }),
44
+ [page, q],
45
+ { key: ["contacts", "list", { page, q }] }, // name EVERY input the fetcher reads
46
+ );
47
+ const contact = useG8Query((s) => g8Get(`/contacts/${id}`, {}, { signal: s }), [id], { key: ["contacts", "detail", id] });
48
+ const save = useG8Mutation((v: Patch) => g8Patch(`/contacts/${v.id}`, v), { invalidates: ["contacts"] });
49
+ ```
50
+
51
+ - **Dedupe**: components asking for the same key share one request and one answer.
52
+ - **Stale-while-revalidate**: a cached key renders instantly on mount (no skeleton) and refreshes in the
53
+ background once older than `staleTime` (default 0). Use `staleTime: 60_000` for data that rarely changes
54
+ (custom-field schemas, pipelines, dispositions).
55
+ - **Focus/reconnect**: watched keys refresh when the window regains focus or the network returns, at most
56
+ once per 5 s per key (`refetchOnFocus: false` opts out; `usePaginated` defaults to false so a scroll is kept).
57
+ - **Invalidation**: `invalidate("contacts")` (or a mutation's `invalidates: ["contacts"]`) marks every key
58
+ starting with `"contacts"` stale; keys on screen refetch now, a request started before the write is
59
+ superseded, the rest refetch when next shown. Prefixes match whole parts (`"deals"` never matches `"deals-x"`).
60
+ - **Writes into the cache**: `setQueryData(key, value | old => new)`; `useG8Mutation(fn, { optimistic: { prefix, update } })`
61
+ changes cached data before the request and puts it back if the write fails.
62
+ - **Memory**: at most 200 keys (LRU); a key nobody watches is dropped after 5 minutes; requests abort when
63
+ their LAST subscriber unmounts.
64
+ - **`placeholderData`**: show a list row while the full record loads (`isPlaceholderData: true`). Never seed
65
+ an edit form from it (edit-before-load PATCHes blanks over real values): enable Edit only when
66
+ `!isPlaceholderData && !loading`.
67
+ - **SSR**: nothing is cached on the server; hooks render their initial state and fetch after hydration.
68
+ - **Keys are JSON-like**: strings, numbers, booleans, null, arrays, plain objects and Dates. A `Map`, `Set`,
69
+ `URLSearchParams` or class instance throws (it would otherwise collide with every other one).
70
+ - **Sign-out and org switch**: the cache is one per page and is not keyed by user or org. Call
71
+ `g8QueryCache.clear()` on sign-out or org switch, and add the org to your keys if you pass `targetOrgId`,
72
+ so one org's cached rows are never shown to another.
73
+
74
+ Key convention used by the blocks: `[area, "list" | "detail" | <sub-resource>, ...inputs]`, area = the API
75
+ path's first segment (`contacts`, `companies`, `deals`, `sequences`, `lists`, `inbox`, `tasks`...), so a write
76
+ to an area invalidates that area with one prefix.
77
+
33
78
  ## Tiers: when to confirm
34
79
 
35
80
  Every operation has a tier (`read`, `write`, `billable`, `external`, `destructive`); each component's
@@ -62,6 +107,21 @@ The proxy therefore **refuses every request (500) until you configure it**:
62
107
  5. Local prototypes only: `dangerouslyAllowUnauthenticated: true` and `dangerouslyAllowAllPaths: true`
63
108
  switch the two gates off. Never ship them.
64
109
 
110
+ ### Local preview (apps made by `graph8-sdk create`)
111
+
112
+ Created apps run before they have sign-in: the route sets `localPreview: true`. While `authorize` is unset the
113
+ proxy answers a request only if NODE_ENV is not `production` AND it comes from this machine: Host is
114
+ `localhost`, `127.0.0.1` or `[::1]` (blocks DNS rebinding), the browser's `Sec-Fetch-Site`/`Origin` say it is
115
+ the app's own page (blocks other sites in the same browser), and no forwarding proxy names a remote client
116
+ (blocks tunnels). `npm run dev` listens on 127.0.0.1, because a machine on your network could forge those
117
+ headers. Other hosts get 403; a production build gets 500 until `authorize` is set. Once `authorize` is set,
118
+ `localPreview` does nothing.
119
+
120
+ The key comes from `G8_API_KEY` (env, `.env.local`) or, in local preview only, the key pasted in the
121
+ Connect graph8 panel, which the server checks against `GET /me` and stores in `.g8/local-key` (0600, git-ignored).
122
+ `npx @graph8/sdk doctor --production` fails until the route is safe to deploy;
123
+ `npx @graph8/sdk doctor --fix-prefixes` regenerates `allowedPrefixes` after you add or remove blocks.
124
+
65
125
  What the proxy does on its own: rejects `..`, encoded slashes and empty segments (400); forwards only
66
126
  `content-type`, `accept`, `accept-language` and `idempotency-key` (never the browser's cookies or
67
127
  `Authorization`); strips hop-by-hop headers, `set-cookie` and `location` from the response; does not follow
@@ -91,6 +151,9 @@ for meaning, no entrance animations, light and dark through the tokens.
91
151
  | `Sparkline` | `sparkline` | Tiny SVG trend for tiles and cells. `data: number[]`, optional `tone`. |
92
152
  | `BarList` | `bar-list` | Ranked rows with a proportional ink bar; `href` makes a row a link. |
93
153
  | `TimeSeriesChart` | `time-series-chart` | `data`, `xKey`, `series: { key, label?, tone? }[]`, `kind` line, area, bar. Needs the `recharts` peer. `tone: "prior"` is the dashed comparison. |
154
+ | `useRowActivation` | `hooks/use-row-activation` | `<TableRow {...rowActivationProps(() => open(row.id))}>` (inside `.map()`; `useRowActivation` is the hook form): the whole row is focusable, Enter/Space/click open it, nested checkboxes, buttons and links keep their own behaviour. Pass `{ role: "button" }` for a non-table row. |
155
+ | `RecordLink` | `components/screen/record-link` | A record name as a real `<a href="?id=X">`: plain click opens in place (URL state), Cmd/Ctrl/middle-click opens a new tab on the same record. `full` adds `view=full`. |
156
+ | `plural`, `pluralWord` | `lib/plural` | `plural(1, "contact")` -> "1 contact", `plural(3, "company", "companies")` -> "3 companies". Never hand-roll `=== 1 ? "" : "s"`. |
94
157
  | `useUrlState` | `hooks/use-url-state` | `const [id, setId] = useUrlState("id")`; `setId(null)` removes it; `{ push: true }` adds a history entry. |
95
158
 
96
159
  A finished list screen, start to end:
@@ -7,10 +7,14 @@
7
7
  "browser-safe API calls via a server proxy",
8
8
  "query, mutation, pagination and job-polling hooks",
9
9
  "spend/send/delete confirmation dialog",
10
- "screen kit: layout, stats, split view, detail panel, toolbar, status pills, avatars, cards, form sheet, charts"
10
+ "screen kit: layout, stats, split view, detail panel, toolbar, status pills, avatars, cards, form sheet, charts",
11
+ "local preview: a Connect graph8 screen stores the server's key for `next dev`; generated allowedPrefixes",
12
+ "white-label: BrandProvider + useBrand give every block's copy and link-outs your platform name and app URL (default graph8)"
11
13
  ],
12
14
  "dependsOn": [],
13
- "operations": [],
15
+ "operations": [
16
+ "describe_current_key_me_get"
17
+ ],
14
18
  "components": [
15
19
  {
16
20
  "name": "ConfirmSpendDialog",
@@ -19,6 +23,13 @@
19
23
  "operations": [],
20
24
  "tiers": []
21
25
  },
26
+ {
27
+ "name": "AskButton",
28
+ "file": "components/confirmed-action.tsx",
29
+ "purpose": "useConfirmAction + AskButton: a button asks, one ConfirmSpendDialog per screen confirms, then the write runs (Cancel is ignored while it runs).",
30
+ "operations": [],
31
+ "tiers": []
32
+ },
22
33
  {
23
34
  "name": "ApiError",
24
35
  "file": "components/api-error.tsx",
@@ -123,23 +134,44 @@
123
134
  "purpose": "Line, area or bar chart over time on recharts: ink series, tones only for meaning, dashed prior period, hairline grid.",
124
135
  "operations": [],
125
136
  "tiers": []
137
+ },
138
+ {
139
+ "name": "RecordLink",
140
+ "file": "components/screen/record-link.tsx",
141
+ "purpose": "A record's name as a real <a href=\"?id=X\">: a plain click opens it in place (URL state), Cmd/Ctrl/middle-click opens a new tab on the same record.",
142
+ "operations": [],
143
+ "tiers": []
144
+ },
145
+ {
146
+ "name": "ConnectGraph8Panel",
147
+ "file": "components/connect-graph8.tsx",
148
+ "purpose": "Local preview only: paste a graph8 API key, the server checks it (GET /me) and stores it in .g8/local-key, never in the browser; replace or remove it. G8ConnectGate shows it until a key is connected and is a no-op in production.",
149
+ "operations": [],
150
+ "tiers": []
151
+ },
152
+ {
153
+ "name": "BrandProvider",
154
+ "file": "components/brand-provider.tsx",
155
+ "purpose": "White-label context: the app's name, logo, and what the platform is called and where it lives, for every block's copy and link-outs. Fields left out stay graph8's.",
156
+ "operations": [],
157
+ "tiers": []
126
158
  }
127
159
  ],
128
160
  "hooks": [
129
161
  {
130
162
  "name": "useG8Query",
131
163
  "file": "hooks/use-g8-query.ts",
132
- "purpose": "Load data with loading/error/data, an out-of-order guard and refetch."
164
+ "purpose": "Load data with loading/error/data, an out-of-order guard and refetch. Optional `key` joins the shared cache (dedupe, stale-while-revalidate, focus refresh, invalidation)."
133
165
  },
134
166
  {
135
167
  "name": "useG8Mutation",
136
168
  "file": "hooks/use-g8-mutation.ts",
137
- "purpose": "Run a write with loading/error state and success/error callbacks."
169
+ "purpose": "Run a write with loading/error state and success/error callbacks. Optional `invalidates` refreshes cached keys; optional `optimistic` update with rollback."
138
170
  },
139
171
  {
140
172
  "name": "usePaginated",
141
173
  "file": "hooks/use-paginated.ts",
142
- "purpose": "Cursor pagination over the API envelope (pagination.next_cursor) with load-more and reload."
174
+ "purpose": "Cursor pagination over the API envelope (pagination.next_cursor) with load-more and reload. Optional `key` caches the loaded pages."
143
175
  },
144
176
  {
145
177
  "name": "useJobPoll",
@@ -150,6 +182,16 @@
150
182
  "name": "useUrlState",
151
183
  "file": "hooks/use-url-state.ts",
152
184
  "purpose": "One string in the URL query as state (replaceState + popstate, SSR-safe, no next/navigation): the open record, tab or search survives reload and Back."
185
+ },
186
+ {
187
+ "name": "useRowActivation",
188
+ "file": "hooks/use-row-activation.ts",
189
+ "purpose": "Props that make a whole list row focusable and openable with Enter/Space and click, leaving nested checkboxes, buttons and links alone. rowActivationProps is the same as a plain function for use inside .map()."
190
+ },
191
+ {
192
+ "name": "useBrand",
193
+ "file": "hooks/use-brand.ts",
194
+ "purpose": "The current brand ({ name, appUrl, linkLabel, logo }) from the nearest BrandProvider, else graph8's defaults; brandUrl(brand, path) builds a link-out."
153
195
  }
154
196
  ],
155
197
  "lib": [
@@ -157,9 +199,25 @@
157
199
  "file": "lib/g8-client.ts",
158
200
  "purpose": "Browser caller for the app's own /api/g8 proxy. Holds no key, returns the SDK envelope, throws G8Error."
159
201
  },
202
+ {
203
+ "file": "components/screen/time-series-chart-recharts.tsx",
204
+ "purpose": "The recharts implementation of TimeSeriesChart, code-split: TimeSeriesChart loads it on first render, so pages without a chart never download recharts."
205
+ },
206
+ {
207
+ "file": "components/screen/time-series-format.ts",
208
+ "purpose": "TimeSeriesChart props, series type and axis formatters (defaultFormatX/defaultFormatY), with no recharts import."
209
+ },
210
+ {
211
+ "file": "lib/g8-cache.ts",
212
+ "purpose": "Shared query cache behind the hooks' optional `key`: one in-flight request per key, stale-while-revalidate, focus/reconnect refresh, invalidate(prefix), setQueryData, optimistic writes, bounded LRU (200) with GC."
213
+ },
160
214
  {
161
215
  "file": "components/screen/index.ts",
162
216
  "purpose": "Barrel for the screen kit: import every part from @/g8/components/screen."
217
+ },
218
+ {
219
+ "file": "lib/plural.ts",
220
+ "purpose": "plural(n, 'contact') -> '1 contact' / '2 contacts' (and pluralWord): one correct plural helper for titles, counts and bulk dialogs."
163
221
  }
164
222
  ],
165
223
  "server": [
@@ -168,10 +226,25 @@
168
226
  "purpose": "createG8Proxy(): forwards browser calls to the graph8 API with the server-side key, a required path allowlist and authorize hook (it refuses to run without them) and a server-side org hook.",
169
227
  "target": "g8/server/proxy.ts"
170
228
  },
229
+ {
230
+ "file": "server/local-key.ts",
231
+ "purpose": "Node only: the local-preview key file (.g8/local-key, 0600, self-gitignored) and resolveApiKey() (environment first, then the file outside production).",
232
+ "target": "g8/server/local-key.ts"
233
+ },
234
+ {
235
+ "file": "server/connect.ts",
236
+ "purpose": "createG8ConnectRoute(): GET status, POST a key (checked with GET /me), DELETE it; answers only next dev requests from this machine, 404 in production.",
237
+ "target": "g8/server/connect.ts"
238
+ },
171
239
  {
172
240
  "file": "server/route.ts",
173
241
  "purpose": "The Next.js App Router catch-all route that mounts the proxy.",
174
242
  "target": "app/api/g8/[...path]/route.ts"
243
+ },
244
+ {
245
+ "file": "server/connect-route.ts",
246
+ "purpose": "The Next.js route behind the Connect graph8 screen.",
247
+ "target": "app/api/g8-connect/route.ts"
175
248
  }
176
249
  ],
177
250
  "recipes": [
@@ -210,7 +283,7 @@
210
283
  },
211
284
  {
212
285
  "decision": "How the proxy knows the caller is a signed-in user of the app",
213
- "default": "None: the route refuses every request until `authorize` is set",
286
+ "default": "Local preview: until `authorize` is set the route answers only `next dev` requests from this machine; a production build refuses every request until `authorize` is set",
214
287
  "askHuman": true
215
288
  },
216
289
  {
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
- import { G8Error } from "@graph8/sdk";
3
+ import { G8Error } from "@graph8/sdk/errors";
4
4
  import { CircleAlert } from "lucide-react";
5
5
  import { Button, cn } from "@graph8/sdk/ui";
6
+ import { DEFAULT_BRAND, useBrand } from "../hooks/use-brand";
6
7
 
7
8
  export interface ApiErrorProps {
8
9
  error: unknown;
@@ -23,22 +24,22 @@ interface FieldError {
23
24
  }
24
25
 
25
26
  /** A readable one-line reason for any thrown value. 422 field errors are listed. */
26
- export function describeError(error: unknown): { message: string; fields: string[]; status?: number; requestId?: string } {
27
+ export function describeError(error: unknown, platform: string = DEFAULT_BRAND.linkLabel): { message: string; fields: string[]; status?: number; requestId?: string } {
27
28
  if (error instanceof G8Error) {
28
29
  const fields = Array.isArray(error.detail)
29
30
  ? (error.detail as FieldError[]).map((f) => `${(f.loc ?? []).filter((p) => p !== "body").join(".")}: ${f.msg ?? ""}`.trim())
30
31
  : [];
31
32
  const message =
32
- typeof error.detail === "string" ? error.detail : fields.length ? "Some fields are invalid." : explain(error);
33
+ typeof error.detail === "string" ? error.detail : fields.length ? "Some fields are invalid." : explain(error, platform);
33
34
  return { message, fields, status: error.status, requestId: error.requestId };
34
35
  }
35
36
  return { message: error instanceof Error ? error.message : "Something went wrong.", fields: [] };
36
37
  }
37
38
 
38
- function explain(error: G8Error): string {
39
- if (error.status === 401) return "Your session with graph8 is not valid. Check the server API key.";
39
+ function explain(error: G8Error, platform: string): string {
40
+ if (error.status === 401) return `Your session with ${platform} is not valid. Check the server API key.`;
40
41
  if (error.status === 403) return "This API key is not allowed to do that (missing scope, or the proxy blocked the path).";
41
- if (error.status === 429) return "graph8 is rate limiting requests. Wait a moment and retry.";
42
+ if (error.status === 429) return `${platform} is rate limiting requests. Wait a moment and retry.`;
42
43
  if (error.status === 0) return "Could not reach the server. Check your connection.";
43
44
  return error.message;
44
45
  }
@@ -48,8 +49,9 @@ function explain(error: G8Error): string {
48
49
  * is an error with Retry, never an empty list. Ember is reserved for this: errors and alerts.
49
50
  */
50
51
  export function ApiError({ error, title = "Something went wrong", onRetry, className, compact, retryLabel = "Retry", actions }: ApiErrorProps) {
52
+ const brand = useBrand();
51
53
  if (error == null) return null;
52
- const info = describeError(error);
54
+ const info = describeError(error, brand.linkLabel);
53
55
 
54
56
  if (compact) {
55
57
  return (
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Wraps the app so every block shows YOUR brand: the platform's name in copy and link-outs, and where those links
5
+ * go. A created app renders it in components/app-shell.tsx from `g8.config.ts`:
6
+ *
7
+ * <BrandProvider brand={brand}>{children}</BrandProvider>
8
+ *
9
+ * Fields left out keep graph8's defaults (see hooks/use-brand.ts). No API calls.
10
+ */
11
+
12
+ import { useMemo, type ReactNode } from "react";
13
+ import { BrandContext, resolveBrand, type BrandConfig } from "../hooks/use-brand";
14
+
15
+ export interface BrandProviderProps {
16
+ brand?: Partial<BrandConfig> | null;
17
+ children: ReactNode;
18
+ }
19
+
20
+ export function BrandProvider({ brand, children }: BrandProviderProps) {
21
+ const value = useMemo(() => resolveBrand(brand), [brand?.name, brand?.appUrl, brand?.linkLabel, brand?.logo]); // eslint-disable-line react-hooks/exhaustive-deps
22
+ return <BrandContext.Provider value={value}>{children}</BrandContext.Provider>;
23
+ }
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ /**
4
+ * useConfirmAction + AskButton (foundation, shared by every block's record detail): the pattern detail screens use for every write. A button calls `ask(config)`; the screen
5
+ * renders ONE `<ConfirmSpendDialog {...dialog} />` (so the confirmation is visible in the screen's own source), the person confirms,
6
+ * then `config.run()` executes and the failure, if any, is shown in `failure`.
7
+ *
8
+ * SDK operations: none of its own; `run` is whatever the caller passes. `tolerateStatus` lists HTTP statuses that count as done
9
+ * (a delete answering 404); `mapError` can turn a thrown error into a clearer one (return null to treat it as done).
10
+ */
11
+ import { useState } from "react";
12
+ import type { ReactNode } from "react";
13
+ import { Button } from "@graph8/sdk/ui";
14
+ import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
15
+
16
+ export interface ConfirmActionConfig {
17
+ tier: ConfirmTier;
18
+ title: string;
19
+ description?: ReactNode;
20
+ confirmLabel?: string;
21
+ estimatedCredits?: number;
22
+ recipients?: number;
23
+ affected?: number;
24
+ tolerateStatus?: number[];
25
+ mapError?: (error: unknown) => unknown;
26
+ run: () => Promise<unknown>;
27
+ onDone?: () => void;
28
+ }
29
+
30
+ export interface AskButtonProps extends ConfirmActionConfig {
31
+ ask: (config: ConfirmActionConfig) => void;
32
+ label: string;
33
+ variant?: "default" | "outline" | "ghost";
34
+ disabled?: boolean;
35
+ }
36
+
37
+ export function useConfirmAction() {
38
+ const [cfg, setCfg] = useState<ConfirmActionConfig | null>(null);
39
+ const [busy, setBusy] = useState(false);
40
+ const [failure, setFailure] = useState<unknown>(null);
41
+
42
+ const go = async () => {
43
+ if (!cfg) return;
44
+ setBusy(true);
45
+ setFailure(null);
46
+ try {
47
+ await cfg.run();
48
+ cfg.onDone?.();
49
+ } catch (err) {
50
+ const status = (err as { status?: number })?.status;
51
+ if (status != null && cfg.tolerateStatus?.includes(status)) cfg.onDone?.();
52
+ else {
53
+ const shown = cfg.mapError ? cfg.mapError(err) : err;
54
+ if (shown == null) cfg.onDone?.();
55
+ else setFailure(shown);
56
+ }
57
+ } finally {
58
+ setBusy(false);
59
+ setCfg(null);
60
+ }
61
+ };
62
+
63
+ return {
64
+ ask: (config: ConfirmActionConfig) => setCfg(config),
65
+ busy,
66
+ failure,
67
+ dialog: {
68
+ open: cfg != null,
69
+ tier: cfg?.tier ?? ("external" as ConfirmTier),
70
+ title: cfg?.title ?? "",
71
+ description: cfg?.description,
72
+ estimatedCredits: cfg?.estimatedCredits,
73
+ recipients: cfg?.recipients,
74
+ affected: cfg?.affected,
75
+ confirmLabel: cfg?.confirmLabel,
76
+ confirming: busy,
77
+ onConfirm: () => void go(),
78
+ // While the write is running, Cancel does nothing: closing then would hide a write that still lands.
79
+ onCancel: () => { if (!busy) setCfg(null); },
80
+ },
81
+ };
82
+ }
83
+
84
+ export function AskButton({ ask, label, variant = "outline", disabled, ...config }: AskButtonProps) {
85
+ return (
86
+ <Button size="sm" variant={variant} disabled={disabled} onClick={() => ask({ confirmLabel: label, ...config })}>
87
+ {label}
88
+ </Button>
89
+ );
90
+ }