@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.
- package/README.md +14 -10
- package/blocks/_foundation/README.md +172 -1
- package/blocks/_foundation/block.json +234 -13
- package/blocks/_foundation/components/api-error.tsx +71 -25
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +39 -7
- package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
- package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
- package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
- package/blocks/_foundation/components/screen/index.ts +22 -0
- package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
- package/blocks/_foundation/components/screen/section-card.tsx +47 -0
- package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
- package/blocks/_foundation/components/screen/split-view.tsx +125 -0
- package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
- package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-state.ts +74 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +21 -1
- package/blocks/appointments/block.json +43 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +77 -0
- package/blocks/appointments/components/meeting-list.tsx +14 -6
- package/blocks/appointments/components/meetings-screen.tsx +157 -0
- package/blocks/appointments/components/scheduling-screen.tsx +122 -0
- package/blocks/appointments/hooks/use-event-types.ts +17 -0
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +26 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +34 -3
- package/blocks/deals/block.json +45 -4
- package/blocks/deals/components/deal-detail.tsx +27 -13
- package/blocks/deals/components/deal-related.tsx +87 -0
- package/blocks/deals/components/deals-screen.tsx +298 -0
- package/blocks/deals/components/pipeline-board.tsx +4 -2
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-default-owner.ts +60 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deals-types.ts +14 -0
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +12 -0
- package/blocks/forms/block.json +39 -17
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +353 -0
- package/blocks/forms/components/submissions-table.tsx +16 -6
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/README.md +16 -0
- package/blocks/lifecycle/block.json +34 -7
- package/blocks/lifecycle/components/account-related.tsx +66 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/README.md +13 -0
- package/blocks/newsletter/block.json +47 -23
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
- package/blocks/newsletter/components/subscriber-table.tsx +12 -6
- package/blocks/nurture/README.md +12 -0
- package/blocks/nurture/block.json +35 -13
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +283 -0
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +12 -0
- package/blocks/pages/block.json +35 -22
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +46 -11
- package/blocks/pages/components/page-list.tsx +6 -3
- package/blocks/pages/components/pages-screen.tsx +154 -0
- package/blocks/quotes/README.md +17 -0
- package/blocks/quotes/block.json +38 -9
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-list.tsx +7 -3
- package/blocks/quotes/components/quote-related.tsx +123 -0
- package/blocks/quotes/components/quotes-screen.tsx +169 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +11 -0
- package/blocks/social-ads/block.json +101 -2
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2785 -1637
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +25 -8
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +20 -0
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +32 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/stack.schema.json +6 -1
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +55 -8
- package/templates/next-app/app/globals.css.tmpl +105 -0
- package/templates/next-app/app/layout.tsx.tmpl +18 -4
- package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
- package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
- package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
- package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/lib/nav.ts.tmpl +1 -1
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { G8Error } from "@graph8/sdk";
|
|
3
|
+
import { G8Error } from "@graph8/sdk/errors";
|
|
4
|
+
import { CircleAlert } from "lucide-react";
|
|
5
|
+
import { Button, cn } from "@graph8/sdk/ui";
|
|
6
|
+
import { DEFAULT_BRAND, useBrand } from "../hooks/use-brand";
|
|
4
7
|
|
|
5
8
|
export interface ApiErrorProps {
|
|
6
9
|
error: unknown;
|
|
@@ -8,6 +11,11 @@ export interface ApiErrorProps {
|
|
|
8
11
|
/** Shows a Retry button. */
|
|
9
12
|
onRetry?: () => void;
|
|
10
13
|
className?: string;
|
|
14
|
+
/** One line with the reason and Retry inline, for toolbars and cards. */
|
|
15
|
+
compact?: boolean;
|
|
16
|
+
retryLabel?: string;
|
|
17
|
+
/** Extra actions beside Retry (open settings, contact support). */
|
|
18
|
+
actions?: React.ReactNode;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
interface FieldError {
|
|
@@ -16,47 +24,85 @@ interface FieldError {
|
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
/** A readable one-line reason for any thrown value. 422 field errors are listed. */
|
|
19
|
-
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 } {
|
|
20
28
|
if (error instanceof G8Error) {
|
|
21
29
|
const fields = Array.isArray(error.detail)
|
|
22
30
|
? (error.detail as FieldError[]).map((f) => `${(f.loc ?? []).filter((p) => p !== "body").join(".")}: ${f.msg ?? ""}`.trim())
|
|
23
31
|
: [];
|
|
24
32
|
const message =
|
|
25
|
-
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);
|
|
26
34
|
return { message, fields, status: error.status, requestId: error.requestId };
|
|
27
35
|
}
|
|
28
36
|
return { message: error instanceof Error ? error.message : "Something went wrong.", fields: [] };
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
function explain(error: G8Error): string {
|
|
32
|
-
if (error.status === 401) return
|
|
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.`;
|
|
33
41
|
if (error.status === 403) return "This API key is not allowed to do that (missing scope, or the proxy blocked the path).";
|
|
34
|
-
if (error.status === 429) return
|
|
42
|
+
if (error.status === 429) return `${platform} is rate limiting requests. Wait a moment and retry.`;
|
|
35
43
|
if (error.status === 0) return "Could not reach the server. Check your connection.";
|
|
36
44
|
return error.message;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Inline error panel for a failed graph8 call. Always shows `detail`, not just `message`. A failed load
|
|
49
|
+
* is an error with Retry, never an empty list. Ember is reserved for this: errors and alerts.
|
|
50
|
+
*/
|
|
51
|
+
export function ApiError({ error, title = "Something went wrong", onRetry, className, compact, retryLabel = "Retry", actions }: ApiErrorProps) {
|
|
52
|
+
const brand = useBrand();
|
|
41
53
|
if (error == null) return null;
|
|
42
|
-
const info = describeError(error);
|
|
54
|
+
const info = describeError(error, brand.linkLabel);
|
|
55
|
+
|
|
56
|
+
if (compact) {
|
|
57
|
+
return (
|
|
58
|
+
<div role="alert" data-testid="api-error" className={cn("flex flex-wrap items-center gap-x-3 gap-y-1 rounded-md border border-error-foreground/20 bg-error px-3 py-2 text-sm text-error-foreground", className)}>
|
|
59
|
+
<CircleAlert className="size-4 shrink-0" strokeWidth={1.5} aria-hidden />
|
|
60
|
+
<span className="min-w-0 flex-1 truncate">
|
|
61
|
+
<span className="font-medium">{title}.</span> {info.message}
|
|
62
|
+
</span>
|
|
63
|
+
{actions}
|
|
64
|
+
{onRetry ? (
|
|
65
|
+
<Button type="button" size="sm" variant="outline" onClick={onRetry} className="h-7 border-error-foreground/30 bg-background text-error-foreground hover:bg-background/70 hover:text-error-foreground">
|
|
66
|
+
{retryLabel}
|
|
67
|
+
</Button>
|
|
68
|
+
) : null}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
43
73
|
return (
|
|
44
|
-
<div role="alert" className={
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
<div role="alert" data-testid="api-error" className={cn("flex gap-3 rounded-lg border border-error-foreground/20 bg-error p-4 text-sm text-error-foreground", className)}>
|
|
75
|
+
<div aria-hidden className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-error-foreground/20 bg-background/60">
|
|
76
|
+
<CircleAlert className="size-4" strokeWidth={1.5} />
|
|
77
|
+
</div>
|
|
78
|
+
<div className="min-w-0 flex-1">
|
|
79
|
+
<p className="font-medium leading-5">{title}</p>
|
|
80
|
+
<p className="mt-1 leading-5 text-error-foreground/90">{info.message}</p>
|
|
81
|
+
{info.fields.length > 0 ? (
|
|
82
|
+
<ul className="mt-2 list-disc space-y-0.5 pl-5 text-error-foreground/90">
|
|
83
|
+
{info.fields.map((line) => (
|
|
84
|
+
<li key={line}>{line}</li>
|
|
85
|
+
))}
|
|
86
|
+
</ul>
|
|
87
|
+
) : null}
|
|
88
|
+
{info.status != null || info.requestId ? (
|
|
89
|
+
<p className="mt-2 text-xs text-error-foreground/70 tabular-nums">
|
|
90
|
+
{info.status != null ? <span>HTTP {info.status}</span> : null}
|
|
91
|
+
{info.status != null && info.requestId ? <span aria-hidden> · </span> : null}
|
|
92
|
+
{info.requestId ? <span>Request id: {info.requestId}</span> : null}
|
|
93
|
+
</p>
|
|
94
|
+
) : null}
|
|
95
|
+
{onRetry || actions ? (
|
|
96
|
+
<div className="mt-3 flex flex-wrap items-center gap-2">
|
|
97
|
+
{onRetry ? (
|
|
98
|
+
<Button type="button" size="sm" variant="outline" onClick={onRetry} className="border-error-foreground/30 bg-background text-error-foreground hover:bg-background/70 hover:text-error-foreground">
|
|
99
|
+
{retryLabel}
|
|
100
|
+
</Button>
|
|
101
|
+
) : null}
|
|
102
|
+
{actions}
|
|
103
|
+
</div>
|
|
104
|
+
) : null}
|
|
105
|
+
</div>
|
|
60
106
|
</div>
|
|
61
107
|
);
|
|
62
108
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Connect graph8 (local preview only): the first-run screen of a created app when the server has no graph8 API
|
|
5
|
+
* key yet, and the place to replace or remove the key later.
|
|
6
|
+
*
|
|
7
|
+
* Calls this app's own `/api/g8-connect` route (g8/server/connect.ts), never graph8 directly:
|
|
8
|
+
* GET status: is a key configured, where from (server environment or the pasted file), whose is it
|
|
9
|
+
* POST { key }: the server checks the key with a free read (GET /me), then stores it in .g8/local-key
|
|
10
|
+
* DELETE remove the stored key
|
|
11
|
+
* No billable, external or destructive graph8 operation (removing the local file only forgets the key here).
|
|
12
|
+
* The key goes from the input to the server once and is never kept in browser state after submit, storage or URL.
|
|
13
|
+
*
|
|
14
|
+
* Exports:
|
|
15
|
+
* - ConnectGraph8Panel: the full-page panel (no required props).
|
|
16
|
+
* - G8ConnectGate: wraps the app; in `next dev` it shows the screen until a key is connected, plus a small
|
|
17
|
+
* "Local preview" button to manage the key. In a production build it renders its children and nothing else.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useCallback, useEffect, useId, useState, type FormEvent, type ReactNode } from "react";
|
|
21
|
+
import { CheckCircle2, ExternalLink, KeyRound, Loader2, X } from "lucide-react";
|
|
22
|
+
import { Button, Input, cn } from "@graph8/sdk/ui";
|
|
23
|
+
import { DEFAULT_BRAND, brandUrl, useBrand } from "../hooks/use-brand";
|
|
24
|
+
|
|
25
|
+
export const CONNECT_PATH = "/api/g8-connect";
|
|
26
|
+
/** Where API keys are managed, on the platform's web app (white-label: see hooks/use-brand). */
|
|
27
|
+
const KEYS_PATH = "/studio/settings?tab=api";
|
|
28
|
+
|
|
29
|
+
export interface ConnectStatus {
|
|
30
|
+
localPreview: boolean;
|
|
31
|
+
connected: boolean;
|
|
32
|
+
source: "env" | "file" | null;
|
|
33
|
+
keyHint?: string;
|
|
34
|
+
keyFile?: string;
|
|
35
|
+
account?: { name?: string; workspace?: string; scopes?: string[] };
|
|
36
|
+
problem?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function readJson(res: Response): Promise<Record<string, unknown>> {
|
|
40
|
+
return (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** GET the status. Null when the route does not exist or refuses (production, not this machine). */
|
|
44
|
+
export async function fetchConnectStatus(): Promise<ConnectStatus | null> {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(CONNECT_PATH, { headers: { accept: "application/json" }, cache: "no-store" });
|
|
47
|
+
if (!res.ok) return null;
|
|
48
|
+
const body = await readJson(res);
|
|
49
|
+
return body.localPreview === true ? (body as unknown as ConnectStatus) : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function accountLine(s: ConnectStatus, platform = DEFAULT_BRAND.linkLabel): string {
|
|
56
|
+
const a = s.account ?? {};
|
|
57
|
+
return [a.name, a.workspace].filter(Boolean).join(" at ") || `a ${platform} key`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ConnectGraph8PanelProps {
|
|
61
|
+
/** Status from a previous GET (the gate passes it); fetched on mount when absent. */
|
|
62
|
+
status?: ConnectStatus | null;
|
|
63
|
+
/** Called after a key is stored or removed, with the new status. */
|
|
64
|
+
onChange?: (status: ConnectStatus) => void;
|
|
65
|
+
className?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Paste, check and store the server's graph8 API key; replace or remove it. */
|
|
69
|
+
export function ConnectGraph8Panel({ status: initial, onChange, className }: ConnectGraph8PanelProps) {
|
|
70
|
+
const [status, setStatus] = useState<ConnectStatus | null | undefined>(initial);
|
|
71
|
+
const [key, setKey] = useState("");
|
|
72
|
+
const [busy, setBusy] = useState<"save" | "remove" | null>(null);
|
|
73
|
+
const [error, setError] = useState<string | null>(null);
|
|
74
|
+
const [replacing, setReplacing] = useState(false);
|
|
75
|
+
const inputId = useId();
|
|
76
|
+
const brand = useBrand();
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (initial === undefined) void fetchConnectStatus().then(setStatus);
|
|
80
|
+
}, [initial]);
|
|
81
|
+
|
|
82
|
+
const apply = (next: ConnectStatus) => {
|
|
83
|
+
setStatus(next);
|
|
84
|
+
setReplacing(false);
|
|
85
|
+
onChange?.(next);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const submit = async (e: FormEvent) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
if (!key.trim() || busy) return;
|
|
91
|
+
setBusy("save");
|
|
92
|
+
setError(null);
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(CONNECT_PATH, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ key: key.trim() }) });
|
|
95
|
+
const body = await readJson(res);
|
|
96
|
+
setKey("");
|
|
97
|
+
if (!res.ok) setError(typeof body.message === "string" ? body.message : `Could not save the key (HTTP ${res.status}).`);
|
|
98
|
+
else apply(body as unknown as ConnectStatus);
|
|
99
|
+
} catch {
|
|
100
|
+
setError("Could not reach this app's server. Is `npm run dev` still running?");
|
|
101
|
+
} finally {
|
|
102
|
+
setBusy(null);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const remove = async () => {
|
|
107
|
+
setBusy("remove");
|
|
108
|
+
setError(null);
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(CONNECT_PATH, { method: "DELETE" });
|
|
111
|
+
const body = await readJson(res);
|
|
112
|
+
if (!res.ok) setError(typeof body.message === "string" ? body.message : `Could not remove the key (HTTP ${res.status}).`);
|
|
113
|
+
else apply(body as unknown as ConnectStatus);
|
|
114
|
+
} catch {
|
|
115
|
+
setError("Could not reach this app's server. Is `npm run dev` still running?");
|
|
116
|
+
} finally {
|
|
117
|
+
setBusy(null);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const connected = Boolean(status?.connected);
|
|
122
|
+
const fromEnv = status?.source === "env";
|
|
123
|
+
const showForm = !fromEnv && (!status?.source || replacing);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<section aria-labelledby={`${inputId}-title`} className={cn("mx-auto flex w-full max-w-md flex-col gap-5 px-4 py-14", className)}>
|
|
127
|
+
<div className="flex flex-col gap-2">
|
|
128
|
+
<div aria-hidden className="flex size-10 items-center justify-center rounded-lg border border-border bg-muted text-foreground">
|
|
129
|
+
<KeyRound className="size-5" strokeWidth={1.5} />
|
|
130
|
+
</div>
|
|
131
|
+
<h1 id={`${inputId}-title`} className="text-lg font-medium text-foreground">
|
|
132
|
+
{connected ? `Connected to ${brand.linkLabel}` : `Connect ${brand.linkLabel}`}
|
|
133
|
+
</h1>
|
|
134
|
+
<p className="text-sm leading-5 text-text-color-muted">
|
|
135
|
+
This app reads and writes your {brand.linkLabel} data through its own server. Paste an API key once: the server checks
|
|
136
|
+
it and keeps it in <code className="text-xs">{status?.keyFile ?? ".g8/local-key"}</code> on this computer. The
|
|
137
|
+
browser never stores it.
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{status?.source ? (
|
|
142
|
+
<div className="flex items-start gap-3 rounded-lg border border-border p-3 text-sm">
|
|
143
|
+
{connected ? (
|
|
144
|
+
<CheckCircle2 aria-hidden className="mt-0.5 size-4 shrink-0 text-positive-foreground" strokeWidth={1.5} />
|
|
145
|
+
) : (
|
|
146
|
+
<X aria-hidden className="mt-0.5 size-4 shrink-0 text-error-foreground" strokeWidth={1.5} />
|
|
147
|
+
)}
|
|
148
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
149
|
+
<span className="text-foreground">{connected ? accountLine(status, brand.linkLabel) : "The key no longer works"}</span>
|
|
150
|
+
<span className="text-xs text-text-color-muted">
|
|
151
|
+
Key {status.keyHint ?? ""} from {fromEnv ? "the server environment (.env.local)" : status.keyFile ?? ".g8/local-key"}
|
|
152
|
+
{status.problem ? `. ${status.problem}` : ""}
|
|
153
|
+
</span>
|
|
154
|
+
{fromEnv ? <span className="text-xs text-text-color-muted">To change it, edit .env.local and restart the dev server.</span> : null}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
) : null}
|
|
158
|
+
|
|
159
|
+
{showForm ? (
|
|
160
|
+
<form onSubmit={submit} className="flex flex-col gap-2">
|
|
161
|
+
<label htmlFor={inputId} className="text-sm font-medium text-foreground">
|
|
162
|
+
{brand.linkLabel} API key
|
|
163
|
+
</label>
|
|
164
|
+
<Input
|
|
165
|
+
id={inputId}
|
|
166
|
+
type="password"
|
|
167
|
+
autoComplete="off"
|
|
168
|
+
spellCheck={false}
|
|
169
|
+
value={key}
|
|
170
|
+
onChange={(e) => setKey(e.target.value)}
|
|
171
|
+
placeholder="Paste the key"
|
|
172
|
+
aria-invalid={error ? true : undefined}
|
|
173
|
+
aria-describedby={error ? `${inputId}-error` : undefined}
|
|
174
|
+
/>
|
|
175
|
+
{error ? (
|
|
176
|
+
<p id={`${inputId}-error`} role="alert" className="text-xs text-error-foreground">
|
|
177
|
+
{error}
|
|
178
|
+
</p>
|
|
179
|
+
) : null}
|
|
180
|
+
<div className="flex items-center gap-2 pt-1">
|
|
181
|
+
<Button type="submit" disabled={!key.trim() || busy !== null}>
|
|
182
|
+
{busy === "save" ? <Loader2 aria-hidden className="size-4 animate-spin" /> : null}
|
|
183
|
+
{busy === "save" ? "Checking" : "Connect"}
|
|
184
|
+
</Button>
|
|
185
|
+
{replacing ? (
|
|
186
|
+
<Button type="button" variant="ghost" onClick={() => setReplacing(false)}>
|
|
187
|
+
Cancel
|
|
188
|
+
</Button>
|
|
189
|
+
) : null}
|
|
190
|
+
</div>
|
|
191
|
+
</form>
|
|
192
|
+
) : status?.source === "file" ? (
|
|
193
|
+
<div className="flex flex-col gap-2">
|
|
194
|
+
<div className="flex items-center gap-2">
|
|
195
|
+
<Button type="button" variant="outline" onClick={() => setReplacing(true)} disabled={busy !== null}>
|
|
196
|
+
Replace key
|
|
197
|
+
</Button>
|
|
198
|
+
<Button type="button" variant="ghost" onClick={() => void remove()} disabled={busy !== null}>
|
|
199
|
+
{busy === "remove" ? <Loader2 aria-hidden className="size-4 animate-spin" /> : null}
|
|
200
|
+
Remove key
|
|
201
|
+
</Button>
|
|
202
|
+
</div>
|
|
203
|
+
{error ? (
|
|
204
|
+
<p role="alert" className="text-xs text-error-foreground">
|
|
205
|
+
{error}
|
|
206
|
+
</p>
|
|
207
|
+
) : null}
|
|
208
|
+
</div>
|
|
209
|
+
) : null}
|
|
210
|
+
|
|
211
|
+
<a href={brandUrl(brand, KEYS_PATH)} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-sm text-foreground underline-offset-4 hover:underline">
|
|
212
|
+
Get a key in {brand.linkLabel} (Settings, API)
|
|
213
|
+
<ExternalLink aria-hidden className="size-3.5" strokeWidth={1.5} />
|
|
214
|
+
</a>
|
|
215
|
+
<p className="text-xs leading-5 text-text-color-muted">
|
|
216
|
+
Local preview: this screen and the data routes only answer requests from this computer while you run the dev
|
|
217
|
+
server. Before you deploy, set <code>authorize</code> in app/api/g8/[...path]/route.ts and the key in your host's
|
|
218
|
+
environment; <code>npx @graph8/sdk doctor --production</code> checks both.
|
|
219
|
+
</p>
|
|
220
|
+
</section>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface G8ConnectGateProps {
|
|
225
|
+
children: ReactNode;
|
|
226
|
+
/** Hide the small "Local preview" button once connected. Default false. */
|
|
227
|
+
hideManageButton?: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* In `next dev`: shows ConnectGraph8Panel instead of the app until a key is connected, and a small button to
|
|
232
|
+
* manage the key afterwards. In a production build (and when /api/g8-connect is missing or refuses) it is
|
|
233
|
+
* exactly its children: no request, no extra markup.
|
|
234
|
+
*/
|
|
235
|
+
export function G8ConnectGate({ children, hideManageButton = false }: G8ConnectGateProps) {
|
|
236
|
+
const dev = process.env.NODE_ENV !== "production";
|
|
237
|
+
const [status, setStatus] = useState<ConnectStatus | null | undefined>(dev ? undefined : null);
|
|
238
|
+
const [managing, setManaging] = useState(false);
|
|
239
|
+
const brand = useBrand();
|
|
240
|
+
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
if (dev) void fetchConnectStatus().then(setStatus);
|
|
243
|
+
}, [dev]);
|
|
244
|
+
|
|
245
|
+
const changed = useCallback((next: ConnectStatus) => {
|
|
246
|
+
setStatus(next);
|
|
247
|
+
// Every screen loaded its data with the old (or no) key: start them again.
|
|
248
|
+
if (next.connected) window.location.reload();
|
|
249
|
+
}, []);
|
|
250
|
+
|
|
251
|
+
if (!status) return <>{children}</>;
|
|
252
|
+
if (!status.connected || managing) {
|
|
253
|
+
return (
|
|
254
|
+
<div className="relative">
|
|
255
|
+
{managing && status.connected ? (
|
|
256
|
+
<Button type="button" variant="ghost" size="sm" className="absolute right-4 top-4" onClick={() => setManaging(false)}>
|
|
257
|
+
Back to the app
|
|
258
|
+
</Button>
|
|
259
|
+
) : null}
|
|
260
|
+
<ConnectGraph8Panel status={status} onChange={changed} />
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return (
|
|
265
|
+
<>
|
|
266
|
+
{children}
|
|
267
|
+
{hideManageButton ? null : (
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
onClick={() => setManaging(true)}
|
|
271
|
+
title={`Local preview, connected as ${accountLine(status, brand.linkLabel)}. Manage the ${brand.linkLabel} key.`}
|
|
272
|
+
className="fixed bottom-3 right-3 z-40 inline-flex h-7 items-center gap-1.5 rounded-md border border-border bg-background px-2 text-xs text-text-color-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
273
|
+
>
|
|
274
|
+
<KeyRound aria-hidden className="size-3.5" strokeWidth={1.5} />
|
|
275
|
+
Local preview
|
|
276
|
+
</button>
|
|
277
|
+
)}
|
|
278
|
+
</>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
+
import { cn } from "@graph8/sdk/ui";
|
|
4
5
|
|
|
5
6
|
export interface EmptyStateProps {
|
|
6
7
|
title: string;
|
|
@@ -9,16 +10,47 @@ export interface EmptyStateProps {
|
|
|
9
10
|
action?: ReactNode;
|
|
10
11
|
icon?: ReactNode;
|
|
11
12
|
className?: string;
|
|
13
|
+
/** A second, quieter action (import, learn more). */
|
|
14
|
+
secondaryAction?: ReactNode;
|
|
15
|
+
/** Less vertical space, for panels and cards. */
|
|
16
|
+
compact?: boolean;
|
|
17
|
+
/** Draw a dashed boundary, so the empty area reads as a place where rows will appear. */
|
|
18
|
+
bordered?: boolean;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Shown when a list is empty. Say what will appear here and offer the first step. First-run copy
|
|
23
|
+
* ("No contacts yet. Import a CSV or add one.") differs from filtered-empty copy ("No contacts match
|
|
24
|
+
* these filters.", with a Clear filters action). An ink icon in a quiet well, one sentence, one action.
|
|
25
|
+
*/
|
|
26
|
+
export function EmptyState({ title, description, action, icon, className, secondaryAction, compact, bordered }: EmptyStateProps) {
|
|
16
27
|
return (
|
|
17
|
-
<div
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
<div
|
|
29
|
+
role="status"
|
|
30
|
+
data-testid="empty-state"
|
|
31
|
+
className={cn(
|
|
32
|
+
"flex flex-col items-center justify-center gap-2 px-6 text-center",
|
|
33
|
+
compact ? "py-8" : "py-14",
|
|
34
|
+
bordered && "rounded-lg border border-dashed border-border",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
{icon ? (
|
|
39
|
+
<div
|
|
40
|
+
aria-hidden
|
|
41
|
+
className="mb-1 flex size-10 items-center justify-center rounded-lg border border-border bg-muted text-foreground [&_svg]:size-5 [&_svg]:stroke-[1.5]"
|
|
42
|
+
>
|
|
43
|
+
{icon}
|
|
44
|
+
</div>
|
|
45
|
+
) : null}
|
|
46
|
+
<h3 className="text-sm font-medium text-foreground">{title}</h3>
|
|
47
|
+
{description ? <p className="max-w-sm text-sm leading-5 text-text-color-muted">{description}</p> : null}
|
|
48
|
+
{action || secondaryAction ? (
|
|
49
|
+
<div className="mt-2 flex flex-wrap items-center justify-center gap-2">
|
|
50
|
+
{secondaryAction}
|
|
51
|
+
{action}
|
|
52
|
+
</div>
|
|
53
|
+
) : null}
|
|
22
54
|
</div>
|
|
23
55
|
);
|
|
24
56
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BarList: ranked rows (top sources, top campaigns, stages) with a proportional ink bar behind each
|
|
5
|
+
* label and the value right-aligned. Rows with `href` are links. The first row carries the story in
|
|
6
|
+
* ink; the rest are an ink tint.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations: none.
|
|
9
|
+
*/
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
import { cn } from "@graph8/sdk/ui";
|
|
12
|
+
import { TONE_CLASSES, type Tone } from "./status-pill";
|
|
13
|
+
|
|
14
|
+
export interface BarListItem {
|
|
15
|
+
label: ReactNode;
|
|
16
|
+
value: number;
|
|
17
|
+
href?: string;
|
|
18
|
+
onClick?: () => void;
|
|
19
|
+
/** Colours this row's bar; only when the row means good or bad. */
|
|
20
|
+
tone?: Tone;
|
|
21
|
+
/** Text shown instead of the raw number ("42%", "1.2k"). */
|
|
22
|
+
display?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BarListProps {
|
|
26
|
+
items: BarListItem[];
|
|
27
|
+
/** Highest value to scale against; defaults to the largest item. */
|
|
28
|
+
max?: number;
|
|
29
|
+
/** Format a value for display when `display` is absent. */
|
|
30
|
+
formatValue?: (value: number) => string;
|
|
31
|
+
/** Emphasise the first row in full ink. Default true. */
|
|
32
|
+
emphasizeFirst?: boolean;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultFormat = (v: number) => v.toLocaleString();
|
|
37
|
+
|
|
38
|
+
export function BarList({ items, max, formatValue = defaultFormat, emphasizeFirst = true, className }: BarListProps) {
|
|
39
|
+
const top = max ?? Math.max(0, ...items.map((i) => i.value));
|
|
40
|
+
return (
|
|
41
|
+
<ol className={cn("flex flex-col gap-1.5", className)} data-testid="bar-list">
|
|
42
|
+
{items.map((item, i) => {
|
|
43
|
+
const pct = top > 0 ? Math.max(0, Math.min(100, (item.value / top) * 100)) : 0;
|
|
44
|
+
const bar = item.tone ? TONE_CLASSES[item.tone].bar + " opacity-30" : emphasizeFirst && i === 0 ? "bg-foreground/15" : "bg-foreground/[0.07]";
|
|
45
|
+
const inner = (
|
|
46
|
+
<>
|
|
47
|
+
<span aria-hidden className={cn("absolute inset-y-0 left-0 rounded-sm transition-[width]", bar)} style={{ width: `${pct}%` }} />
|
|
48
|
+
<span className="relative min-w-0 flex-1 truncate text-sm text-foreground">{item.label}</span>
|
|
49
|
+
<span className="relative shrink-0 pl-3 text-sm tabular-nums text-foreground">{item.display ?? formatValue(item.value)}</span>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
const cls = "relative flex h-7 w-full items-center overflow-hidden rounded-sm px-2 text-left";
|
|
53
|
+
return (
|
|
54
|
+
<li key={i} className="min-w-0">
|
|
55
|
+
{item.href ? (
|
|
56
|
+
<a href={item.href} className={cn(cls, "hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring")}>
|
|
57
|
+
{inner}
|
|
58
|
+
</a>
|
|
59
|
+
) : item.onClick ? (
|
|
60
|
+
<button type="button" onClick={item.onClick} className={cn(cls, "hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring")}>
|
|
61
|
+
{inner}
|
|
62
|
+
</button>
|
|
63
|
+
) : (
|
|
64
|
+
<div className={cls}>{inner}</div>
|
|
65
|
+
)}
|
|
66
|
+
</li>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</ol>
|
|
70
|
+
);
|
|
71
|
+
}
|