@graph8/sdk 0.370.0 → 0.374.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 +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +165 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +57 -20
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
package/README.md
CHANGED
|
@@ -499,10 +499,10 @@ npx @graph8/sdk create mailchimp my-app # then: cd my-app && cp .env.local.exa
|
|
|
499
499
|
| `mailchimp` | Newsletter, nurture, forms, contacts, companies, tasks, lists, suppressions |
|
|
500
500
|
| `clay` | Prospect search, workbench, enrichment, lists, contacts, companies, tasks, suppressions |
|
|
501
501
|
| `6sense` | Intent, visitors, competitors, companies, contacts, tasks |
|
|
502
|
-
| `intercom` | Inbox, web chat conversations, contacts, companies, tasks, AI agents, mailboxes |
|
|
502
|
+
| `intercom` | Inbox, web chat conversations, contacts, companies, tasks, AI agents, mailboxes, email signatures |
|
|
503
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
|
|
505
|
-
| `outreach` | Analytics, contacts, companies, lists, sequences, campaigns, inbox, dialer, call log, meetings, scheduling, tasks, mailboxes, suppressions |
|
|
504
|
+
| `pipedrive` | Analytics, deals, quotes, contacts, companies, inbox, meetings, scheduling, tasks, mailboxes and email signatures; 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, email signatures, suppressions |
|
|
506
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
507
|
| `gong` | Analytics, call log, coaching, dialer, meetings, scheduling, contacts, companies, tasks, lists, suppressions |
|
|
508
508
|
| `salesforce` | Analytics, contacts, companies, lists, deals, quotes, lifecycle, tasks, workflows, copilot, custom objects, suppressions, integrations |
|
|
@@ -512,6 +512,25 @@ npx @graph8/sdk create mailchimp my-app # then: cd my-app && cp .env.local.exa
|
|
|
512
512
|
|
|
513
513
|
Every available stack is proven by `npm run build && npm run e2e:create -- <stack>`, which packs the SDK, scaffolds the app with the built CLI, installs it and requires `next build` to pass. More stacks appear here as their blocks land; `create` refuses a stack that is not available yet.
|
|
514
514
|
|
|
515
|
+
### Branding: your Brand Kit, automatically
|
|
516
|
+
|
|
517
|
+
If your graph8 organization already has a Brand Kit (Studio), `create` uses it. With a key in `G8_API_KEY` (environment or `.env.local`), the new app gets your organization's name, primary colour and logo without any flags; the logo is downloaded into `public/brand/` (never hot-linked), and the colour becomes `--brand` / `--primary` with button text checked for WCAG AA.
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
G8_API_KEY=... npx @graph8/sdk create apollo my-app # picks up the Brand Kit
|
|
521
|
+
npx @graph8/sdk create apollo my-app --no-org-brand # skip it
|
|
522
|
+
npx @graph8/sdk create apollo my-app --brand-color "#0f62fe" # your colour wins; the kit still fills the name and logo
|
|
523
|
+
npx @graph8/sdk brand --from-org # brand an existing app from the kit
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
| Brand Kit field (Developer API) | App brand input |
|
|
527
|
+
|---|---|
|
|
528
|
+
| `GET /me` `org_name` | `--name` (`g8.config.ts` `name`) |
|
|
529
|
+
| `GET /brand-kit` `design_tokens.colors.primary` (else `brand`, `accent`, button background) | `--brand-color` |
|
|
530
|
+
| `GET /brand-kit` `logo_url` (one logo; no light, dark or icon variant) | `--logo` (`public/brand/logo.*`) |
|
|
531
|
+
|
|
532
|
+
Explicit flags always win, field by field. With no key, a missing or empty kit, no `campaigns:read` scope or no network, the app keeps the default look and the CLI prints one info line; it never fails. The key is never printed or written. Fonts, tagline and voice traits are in the kit but have no brand input, so they are not applied. Icons stay Lucide.
|
|
533
|
+
|
|
515
534
|
## UI kit (`@graph8/sdk/ui`)
|
|
516
535
|
|
|
517
536
|
The React component kit and design tokens behind the graph8 app (primitives, light and dark
|
|
@@ -10,6 +10,8 @@ Shared base every other block depends on. Install it first (the CLI does it for
|
|
|
10
10
|
| `server/proxy.ts` | `createG8Proxy(config)`: the logic of the proxy (forwarding, allowlist, `authorize` hook). |
|
|
11
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
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
|
+
| `server/auth.ts` | Optional `authorize` presets (sign-in) for the proxy: `sharedPassword`, `basicAuth`, `jwtAuth`, `sessionAuth`, `allowAnonymous`, and `createG8AuthRoute`. See "Sign-in presets" below. |
|
|
14
|
+
| `components/auth-gate.tsx` | `G8AuthGate`, `SignInPanel`, `G8SignOutButton`: the branded sign-in screen for apps that use a preset. |
|
|
13
15
|
| `components/connect-graph8.tsx` | `ConnectGraph8Panel` + `G8ConnectGate`: the first-run Connect graph8 panel in `next dev`; a no-op in production. |
|
|
14
16
|
| `hooks/use-g8-query.ts` | `useG8Query(fetcher, deps)` -> `{ data, error, loading, refetch }`. Latest request wins. |
|
|
15
17
|
| `hooks/use-g8-mutation.ts` | `useG8Mutation(fn)` -> `{ mutate, mutateAsync, data, error, loading, reset }`. |
|
|
@@ -122,6 +124,51 @@ Connect graph8 panel, which the server checks against `GET /me` and stores in `.
|
|
|
122
124
|
`npx @graph8/sdk doctor --production` fails until the route is safe to deploy;
|
|
123
125
|
`npx @graph8/sdk doctor --fix-prefixes` regenerates `allowedPrefixes` after you add or remove blocks.
|
|
124
126
|
|
|
127
|
+
### Sign-in presets (optional): going live with `authorize`
|
|
128
|
+
|
|
129
|
+
`authorize` is any function `(request) => Response | void`; your own session check is always fine. For the
|
|
130
|
+
common cases `server/auth.ts` ships presets, and `npx @graph8/sdk create <stack> <dir> --auth <preset>` wires one
|
|
131
|
+
up (g8.auth.ts, the route's `authorize`, the sign-in screen, README and `.env.local.example`). Nothing changes for
|
|
132
|
+
apps that do not use one: without `authorize` the proxy behaves exactly as described above.
|
|
133
|
+
|
|
134
|
+
| Preset | What it checks | Server environment |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `sharedPassword()` | one team password; signs an HttpOnly, SameSite=Lax session cookie (HMAC-SHA-256, 7 days); sign-in screen | `G8_APP_PASSWORD` (12+), `G8_SESSION_SECRET` (32+) |
|
|
137
|
+
| `basicAuth()` | HTTP Basic auth; pair with a `middleware.ts` so every page asks; refuses cross-site writes; 20 wrong tries per client per 15 min | `G8_BASIC_AUTH` (`user:pass,user2:pass2`; passwords 12+ characters, no commas) |
|
|
138
|
+
| `jwtAuth()` | a JWT from your OIDC provider (JWKS: RS/PS/ES256 family, discovered from the issuer) or an HS256 secret; exp, nbf, iss, aud; optional `claims` check | `G8_JWT_ISSUER`, `G8_JWT_JWKS_URL`, `G8_JWT_AUDIENCE`, `G8_JWT_COOKIE` or a Bearer header, `G8_JWT_SECRET` |
|
|
139
|
+
| `sessionAuth(fn)` | your session library: return the user to allow (Clerk `auth()`, NextAuth `auth()`, Supabase `getUser()`) | `G8_SIGN_IN_URL`, `G8_SIGN_OUT_URL` (for the screen) |
|
|
140
|
+
| `allowAnonymous()` | nothing: anyone who can reach the app uses the key. Logs a warning; `doctor` flags it | |
|
|
141
|
+
|
|
142
|
+
Wire one by hand in an existing app:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// g8.auth.ts
|
|
146
|
+
import { sharedPassword } from "@/components/g8/_foundation/server/auth";
|
|
147
|
+
export const appAuth = sharedPassword();
|
|
148
|
+
|
|
149
|
+
// app/api/g8/[...path]/route.ts: inside createG8Proxy({ ... })
|
|
150
|
+
authorize: appAuth.authorize,
|
|
151
|
+
|
|
152
|
+
// app/api/g8-auth/route.ts (the sign-in screen's server side; password, jwt and session presets)
|
|
153
|
+
import { createG8AuthRoute } from "@/components/g8/_foundation/server/auth";
|
|
154
|
+
import { appAuth } from "@/g8.auth";
|
|
155
|
+
export const { GET, POST, DELETE } = createG8AuthRoute(appAuth);
|
|
156
|
+
|
|
157
|
+
// app/layout.tsx: wrap the shell (BrandProvider keeps the screen on brand)
|
|
158
|
+
<BrandProvider brand={brand}><G8AuthGate><AppShell headerSlot={<G8SignOutButton />}>...</AppShell></G8AuthGate></BrandProvider>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Security: passwords and secrets are compared in constant time; cookie and Basic presets refuse cross-site writes (CSRF:
|
|
162
|
+
`Sec-Fetch-Site`/`Origin`); JWTs with alg `none`, an algorithm outside the allow-list, a key of the wrong type
|
|
163
|
+
(no HS/RS confusion), `crit` headers or no `exp` are refused; sign-in attempts are rate limited per client (in
|
|
164
|
+
memory: best effort on serverless, so pick a long password). `sharedPassword` also caps WRONG passwords from all
|
|
165
|
+
clients together (`maxFailuresGlobal`, default 100 per 15 min): anyone who can reach the sign-in route can keep
|
|
166
|
+
NEW sign-ins paused that way (existing sessions keep working); with a long random password raise it or pass
|
|
167
|
+
`Infinity`. `jwtAuth` with a JWKS and no audience accepts any token the issuer minted for other apps on the same
|
|
168
|
+
tenant: set `G8_JWT_AUDIENCE` or a `claims` check (it warns at startup, and `doctor` warns). `sharedPassword` and
|
|
169
|
+
`basicAuth` read the environment on every request; `jwtAuth` and `sessionAuth` once at startup. A missing setting is a 500 that names the variable,
|
|
170
|
+
never its value. The sign-in screen is UX only: the proxy's `authorize` is the gate.
|
|
171
|
+
|
|
125
172
|
What the proxy does on its own: rejects `..`, encoded slashes and empty segments (400); forwards only
|
|
126
173
|
`content-type`, `accept`, `accept-language` and `idempotency-key` (never the browser's cookies or
|
|
127
174
|
`Authorization`); strips hop-by-hop headers, `set-cookie` and `location` from the response; does not follow
|
|
@@ -238,6 +285,20 @@ Rules the kit encodes, so screens stay consistent:
|
|
|
238
285
|
- Loading is visible and distinct from empty: `StatStrip loading`, `Skeleton` rows.
|
|
239
286
|
- Dates show short text; the full timestamp is the `title` of a `<time dateTime>`.
|
|
240
287
|
|
|
288
|
+
## Long lists: paging, notices and filters (no silent truncation)
|
|
289
|
+
|
|
290
|
+
A list read that sends a `limit` must page or say it stopped. The foundation ships the pieces:
|
|
291
|
+
|
|
292
|
+
| Need | Use |
|
|
293
|
+
|---|---|
|
|
294
|
+
| Show rows with a "Load more" button (the API pages by `page` or `offset`) | `useLoadMoreList(byPage(size, read))` or `byOffset(size, read)` (`lib/paging.ts`), then `<LoadMore>` under the rows |
|
|
295
|
+
| The API has only a `limit` (no paging) | `<FirstNotice shown={rows.length} limit={LIMIT} noun="mailboxes" />`: "Showing the first N ..." appears only when the read came back full |
|
|
296
|
+
| A screen needs EVERY row (a lookup by id, a full dropdown) | `useAllPages(read, deps)`: reads page after page, reports `truncated` if the page cap cut it short |
|
|
297
|
+
| Choose one audience list | crm's `ListSelect`: a native select with server search and "Load more lists" |
|
|
298
|
+
| Filters for a list screen | `useUrlFilters(keys, { prefix })` keeps them in the URL (`c_email=...`); `TextFilter`, `SelectFilter`, `RangeFilter` and `MoreFilters` (a "More filters" disclosure) go in the Toolbar `filters` slot |
|
|
299
|
+
|
|
300
|
+
`tests/blocks/list-paging-guard.test.ts` in the SDK repo fails on a new `g8Get(..., { limit })` read that does none of these.
|
|
301
|
+
|
|
241
302
|
## Prerequisites
|
|
242
303
|
|
|
243
304
|
- `G8_API_KEY` in the server env; verify with `recipes/verify-proxy-key.ts`.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"spend/send/delete confirmation dialog",
|
|
10
10
|
"screen kit: layout, stats, split view, detail panel, toolbar, status pills, avatars, cards, form sheet, charts",
|
|
11
11
|
"local preview: a Connect graph8 screen stores the server's key for `next dev`; generated allowedPrefixes",
|
|
12
|
+
"go live: optional sign-in presets for the proxy's authorize (team password, Basic, OIDC/JWT, Clerk/NextAuth/Supabase sessions) and a branded sign-in screen",
|
|
12
13
|
"white-label: BrandProvider + useBrand give every block's copy and link-outs your platform name and app URL (default graph8)"
|
|
13
14
|
],
|
|
14
15
|
"dependsOn": [],
|
|
@@ -149,12 +150,40 @@
|
|
|
149
150
|
"operations": [],
|
|
150
151
|
"tiers": []
|
|
151
152
|
},
|
|
153
|
+
{
|
|
154
|
+
"name": "G8AuthGate",
|
|
155
|
+
"file": "components/auth-gate.tsx",
|
|
156
|
+
"purpose": "Sign-in for apps with an auth preset (create --auth): G8AuthGate shows SignInPanel (team password form, provider redirect, or the missing server setting) until the visitor is signed in, G8SignOutButton for the header. Talks only to the app's /api/g8-auth route; renders the app unchanged when that route does not exist.",
|
|
157
|
+
"operations": [],
|
|
158
|
+
"tiers": []
|
|
159
|
+
},
|
|
152
160
|
{
|
|
153
161
|
"name": "BrandProvider",
|
|
154
162
|
"file": "components/brand-provider.tsx",
|
|
155
163
|
"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
164
|
"operations": [],
|
|
157
165
|
"tiers": []
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"name": "ListSelect",
|
|
169
|
+
"file": "components/list-select.tsx",
|
|
170
|
+
"purpose": "Audience-list dropdown, searchable and paged (native select + server search + Load more). It owns no API call: the block that uses it passes a `read` (crm ships ListSelect with the /lists read wired in).",
|
|
171
|
+
"operations": [],
|
|
172
|
+
"tiers": []
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"name": "FirstNotice",
|
|
176
|
+
"file": "components/paging-notices.tsx",
|
|
177
|
+
"purpose": "FirstNotice (\"Showing the first N ...\" when a read without paging came back full) and LoadMore (\"Showing N\" + Load more): the two honest endings of a capped list.",
|
|
178
|
+
"operations": [],
|
|
179
|
+
"tiers": []
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"name": "TextFilter",
|
|
183
|
+
"file": "components/screen/filter-controls.tsx",
|
|
184
|
+
"purpose": "Toolbar filter controls: TextFilter, SelectFilter, RangeFilter (from/to) and MoreFilters (a disclosure that opens by itself when a hidden filter is set).",
|
|
185
|
+
"operations": [],
|
|
186
|
+
"tiers": []
|
|
158
187
|
}
|
|
159
188
|
],
|
|
160
189
|
"hooks": [
|
|
@@ -192,6 +221,21 @@
|
|
|
192
221
|
"name": "useBrand",
|
|
193
222
|
"file": "hooks/use-brand.ts",
|
|
194
223
|
"purpose": "The current brand ({ name, appUrl, linkLabel, logo }) from the nearest BrandProvider, else graph8's defaults; brandUrl(brand, path) builds a link-out."
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "useAllPages",
|
|
227
|
+
"file": "hooks/use-all-pages.ts",
|
|
228
|
+
"purpose": "Read every page of a list a screen needs in full (lookups, full dropdowns), up to a page cap, and report when the cap cut it short."
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"name": "useLoadMoreList",
|
|
232
|
+
"file": "hooks/use-load-more-list.ts",
|
|
233
|
+
"purpose": "A list read with Load more: the shape of a useG8Query list (data, loading, error, refetch) plus hasMore / loadMore. Pair with byPage / byOffset and the LoadMore notice."
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"name": "useUrlFilters",
|
|
237
|
+
"file": "hooks/use-url-filters.ts",
|
|
238
|
+
"purpose": "List-screen filters kept in the URL (debounced, prefixed, SSR-safe): draft for the inputs, query for the read, clear()."
|
|
195
239
|
}
|
|
196
240
|
],
|
|
197
241
|
"lib": [
|
|
@@ -218,6 +262,14 @@
|
|
|
218
262
|
{
|
|
219
263
|
"file": "lib/plural.ts",
|
|
220
264
|
"purpose": "plural(n, 'contact') -> '1 contact' / '2 contacts' (and pluralWord): one correct plural helper for titles, counts and bulk dialogs."
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"file": "lib/paging.ts",
|
|
268
|
+
"purpose": "byPage / byOffset adapt page-number and offset lists to usePaginated's cursor shape; hasNextOf reads the API's has_next / next_cursor."
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"file": "lib/use-debounced-value.ts",
|
|
272
|
+
"purpose": "useDebounced: a value after it stopped changing (foundation-local copy)."
|
|
221
273
|
}
|
|
222
274
|
],
|
|
223
275
|
"server": [
|
|
@@ -226,6 +278,11 @@
|
|
|
226
278
|
"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.",
|
|
227
279
|
"target": "g8/server/proxy.ts"
|
|
228
280
|
},
|
|
281
|
+
{
|
|
282
|
+
"file": "server/auth.ts",
|
|
283
|
+
"purpose": "Optional authorize presets for the proxy: sharedPassword (signed session cookie + sign-in route), basicAuth, jwtAuth (OIDC JWKS or HS256), sessionAuth (Clerk/NextAuth/Supabase adapter), allowAnonymous; createG8AuthRoute() backs the sign-in screen. Web APIs only (Node and Edge).",
|
|
284
|
+
"target": "g8/server/auth.ts"
|
|
285
|
+
},
|
|
229
286
|
{
|
|
230
287
|
"file": "server/local-key.ts",
|
|
231
288
|
"purpose": "Node only: the local-preview key file (.g8/local-key, 0600, self-gitignored) and resolveApiKey() (environment first, then the file outside production).",
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sign-in for a created app that uses an auth preset (`create --auth <preset>`, g8/server/auth.ts). The proxy
|
|
5
|
+
* enforces sign-in on the server; this is the screen people see instead of empty, refused data.
|
|
6
|
+
*
|
|
7
|
+
* Calls this app's own `/api/g8-auth` route (createG8AuthRoute), never graph8:
|
|
8
|
+
* GET the session status: signed in or not, which preset, the sign-in URL, or what the server is missing
|
|
9
|
+
* POST { password } (shared-password preset): sets an HttpOnly session cookie
|
|
10
|
+
* DELETE sign out (shared-password preset)
|
|
11
|
+
* No billable, external or destructive graph8 operation. The password goes from the input to the server once
|
|
12
|
+
* and is never kept in browser state after submit, storage or the URL.
|
|
13
|
+
*
|
|
14
|
+
* Exports:
|
|
15
|
+
* - G8AuthGate: wraps the app. Shows a loading screen while it checks, SignInPanel when signed out, the app when
|
|
16
|
+
* signed in. Re-checks when the tab becomes visible again. If /api/g8-auth does not exist it renders the
|
|
17
|
+
* app unchanged (apps without a preset).
|
|
18
|
+
* - SignInPanel: the sign-in screen (password form, provider redirect, or the setup problem). No required props.
|
|
19
|
+
* - G8SignOutButton: a header button (AppShell `headerSlot`); renders nothing outside a signed-in gate.
|
|
20
|
+
* - useG8Auth: { status, signOut, refresh } inside the gate, else null.
|
|
21
|
+
* The app's name and logo come from the nearest BrandProvider (g8.config.ts), the colours from the theme tokens
|
|
22
|
+
* (the Brand Kit region in globals.css), so the screen is on brand without edits.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createContext, useCallback, useContext, useEffect, useId, useRef, useState, type FormEvent, type ReactNode } from "react";
|
|
26
|
+
import { AlertTriangle, ArrowRight, Eye, EyeOff, Loader2, LockKeyhole, LogIn, LogOut, RefreshCw } from "lucide-react";
|
|
27
|
+
import { Button, Input, cn } from "@graph8/sdk/ui";
|
|
28
|
+
import { useBrand } from "../hooks/use-brand";
|
|
29
|
+
|
|
30
|
+
export const AUTH_PATH = "/api/g8-auth";
|
|
31
|
+
|
|
32
|
+
export type AuthKind = "password" | "basic" | "jwt" | "session" | "anonymous";
|
|
33
|
+
|
|
34
|
+
export interface AuthStatus {
|
|
35
|
+
kind: AuthKind;
|
|
36
|
+
signedIn: boolean;
|
|
37
|
+
signInUrl?: string;
|
|
38
|
+
signOutUrl?: string;
|
|
39
|
+
problem?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The status, null when the route does not exist (no preset), or "error" when the server could not be reached. */
|
|
43
|
+
export async function fetchAuthStatus(): Promise<AuthStatus | null | "error"> {
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(AUTH_PATH, { headers: { accept: "application/json" }, cache: "no-store", credentials: "same-origin" });
|
|
46
|
+
if (res.status === 404) return null;
|
|
47
|
+
if (!res.ok) return "error";
|
|
48
|
+
const body = (await res.json().catch(() => null)) as AuthStatus | null;
|
|
49
|
+
return body && typeof body.signedIn === "boolean" ? body : "error";
|
|
50
|
+
} catch {
|
|
51
|
+
return "error";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface AuthContextValue {
|
|
56
|
+
status: AuthStatus;
|
|
57
|
+
signOut: () => Promise<void>;
|
|
58
|
+
refresh: () => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
62
|
+
|
|
63
|
+
/** Inside G8AuthGate: the session and its actions. Null elsewhere. */
|
|
64
|
+
export function useG8Auth(): AuthContextValue | null {
|
|
65
|
+
return useContext(AuthContext);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Only same-app paths or http(s) URLs are followed; anything else (javascript:, data:) is dropped. */
|
|
69
|
+
function safeUrl(url: string | undefined): string | undefined {
|
|
70
|
+
if (!url) return undefined;
|
|
71
|
+
if (url.startsWith("/") && !url.startsWith("//")) return url;
|
|
72
|
+
return /^https?:\/\//i.test(url) ? url : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Frame({ children, labelledBy }: { children: ReactNode; labelledBy?: string }) {
|
|
76
|
+
return (
|
|
77
|
+
<main aria-labelledby={labelledBy} className="flex min-h-dvh items-center justify-center bg-background px-4 py-12">
|
|
78
|
+
<div className="w-full max-w-sm">{children}</div>
|
|
79
|
+
</main>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function BrandMark() {
|
|
84
|
+
const brand = useBrand();
|
|
85
|
+
return (
|
|
86
|
+
<div className="flex items-center gap-2.5">
|
|
87
|
+
{brand.logo ? (
|
|
88
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
89
|
+
<img src={brand.logo} alt="" className="size-8 rounded-md object-contain" />
|
|
90
|
+
) : (
|
|
91
|
+
<span aria-hidden className="flex size-8 items-center justify-center rounded-md bg-primary text-sm font-medium text-primary-foreground">
|
|
92
|
+
{brand.name.trim().charAt(0).toUpperCase() || "A"}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
<span className="truncate text-sm font-medium text-foreground">{brand.name}</span>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ErrorLine({ id, children }: { id?: string; children: ReactNode }) {
|
|
101
|
+
return (
|
|
102
|
+
<p id={id} role="alert" className="flex items-start gap-1.5 text-xs leading-5 text-error-foreground">
|
|
103
|
+
<AlertTriangle aria-hidden className="mt-0.5 size-3.5 shrink-0" strokeWidth={1.5} />
|
|
104
|
+
<span>{children}</span>
|
|
105
|
+
</p>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function PasswordForm({ onSignedIn }: { onSignedIn: (s: AuthStatus) => void }) {
|
|
110
|
+
const [password, setPassword] = useState("");
|
|
111
|
+
const [show, setShow] = useState(false);
|
|
112
|
+
const [busy, setBusy] = useState(false);
|
|
113
|
+
const [error, setError] = useState<string | null>(null);
|
|
114
|
+
const id = useId();
|
|
115
|
+
|
|
116
|
+
const submit = async (e: FormEvent) => {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
if (!password || busy) return;
|
|
119
|
+
setBusy(true);
|
|
120
|
+
setError(null);
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(AUTH_PATH, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ password }) });
|
|
123
|
+
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
124
|
+
setPassword("");
|
|
125
|
+
if (res.ok && body.signedIn === true) onSignedIn(body as unknown as AuthStatus);
|
|
126
|
+
else setError(typeof body.message === "string" ? body.message : `Could not sign in (HTTP ${res.status}).`);
|
|
127
|
+
} catch {
|
|
128
|
+
setError("Could not reach this app's server. Check your connection and try again.");
|
|
129
|
+
} finally {
|
|
130
|
+
setBusy(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<form onSubmit={submit} className="flex flex-col gap-3" noValidate>
|
|
136
|
+
<div className="flex flex-col gap-1.5">
|
|
137
|
+
<label htmlFor={`${id}-pw`} className="text-sm font-medium text-foreground">
|
|
138
|
+
Password
|
|
139
|
+
</label>
|
|
140
|
+
<div className="relative">
|
|
141
|
+
<Input
|
|
142
|
+
id={`${id}-pw`}
|
|
143
|
+
type={show ? "text" : "password"}
|
|
144
|
+
autoComplete="current-password"
|
|
145
|
+
autoFocus
|
|
146
|
+
value={password}
|
|
147
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
148
|
+
className="pr-10"
|
|
149
|
+
aria-invalid={error ? true : undefined}
|
|
150
|
+
aria-describedby={error ? `${id}-err` : undefined}
|
|
151
|
+
/>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={() => setShow((v) => !v)}
|
|
155
|
+
aria-label={show ? "Hide password" : "Show password"}
|
|
156
|
+
aria-pressed={show}
|
|
157
|
+
className="absolute inset-y-0 right-0 flex w-10 items-center justify-center rounded-r-md text-text-color-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
158
|
+
>
|
|
159
|
+
{show ? <EyeOff aria-hidden className="size-4" strokeWidth={1.5} /> : <Eye aria-hidden className="size-4" strokeWidth={1.5} />}
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
{error ? <ErrorLine id={`${id}-err`}>{error}</ErrorLine> : null}
|
|
164
|
+
<Button type="submit" className="w-full" disabled={!password || busy}>
|
|
165
|
+
{busy ? <Loader2 aria-hidden className="size-4 animate-spin" /> : <ArrowRight aria-hidden className="size-4" strokeWidth={1.5} />}
|
|
166
|
+
{busy ? "Signing in" : "Sign in"}
|
|
167
|
+
</Button>
|
|
168
|
+
</form>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface SignInPanelProps {
|
|
173
|
+
/** Status from a previous GET (the gate passes it); fetched on mount when absent. */
|
|
174
|
+
status?: AuthStatus | null;
|
|
175
|
+
/** Called after a successful sign-in. */
|
|
176
|
+
onSignedIn?: (status: AuthStatus) => void;
|
|
177
|
+
className?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The sign-in screen: a password form, a "Continue to sign in" link to your provider, or what the server lacks. */
|
|
181
|
+
export function SignInPanel({ status: initial, onSignedIn, className }: SignInPanelProps) {
|
|
182
|
+
const [status, setStatus] = useState<AuthStatus | null | undefined>(initial);
|
|
183
|
+
const brand = useBrand();
|
|
184
|
+
const titleId = useId();
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (initial === undefined) void fetchAuthStatus().then((s) => setStatus(s === "error" ? null : s));
|
|
188
|
+
}, [initial]);
|
|
189
|
+
|
|
190
|
+
const signInUrl = safeUrl(status?.signInUrl);
|
|
191
|
+
const problem = status?.problem;
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<section aria-labelledby={titleId} className={cn("flex flex-col gap-6", className)}>
|
|
195
|
+
<BrandMark />
|
|
196
|
+
<div className="flex flex-col gap-1.5">
|
|
197
|
+
<div aria-hidden className="mb-1 flex size-10 items-center justify-center rounded-lg border border-border bg-muted text-foreground">
|
|
198
|
+
{problem ? <AlertTriangle className="size-5" strokeWidth={1.5} /> : <LockKeyhole className="size-5" strokeWidth={1.5} />}
|
|
199
|
+
</div>
|
|
200
|
+
<h1 id={titleId} className="text-lg font-medium text-foreground">
|
|
201
|
+
{problem ? "Sign-in is not set up yet" : `Sign in to ${brand.name}`}
|
|
202
|
+
</h1>
|
|
203
|
+
<p className="text-sm leading-5 text-text-color-muted">
|
|
204
|
+
{problem
|
|
205
|
+
? "This app asks people to sign in before it shows any data, but its server is missing a setting."
|
|
206
|
+
: status?.kind === "password"
|
|
207
|
+
? "Enter the team password to continue."
|
|
208
|
+
: "Use your account to continue."}
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{status === undefined ? (
|
|
213
|
+
<div className="flex h-10 items-center gap-2 text-sm text-text-color-muted" role="status">
|
|
214
|
+
<Loader2 aria-hidden className="size-4 animate-spin" />
|
|
215
|
+
Checking your session
|
|
216
|
+
</div>
|
|
217
|
+
) : problem ? (
|
|
218
|
+
<div className="rounded-lg border border-border bg-muted/40 p-3 text-xs leading-5 text-foreground">
|
|
219
|
+
<p>{problem}</p>
|
|
220
|
+
<p className="mt-2 text-text-color-muted">The settings go in the server environment (.env.local while developing), never in NEXT_PUBLIC_ variables.</p>
|
|
221
|
+
</div>
|
|
222
|
+
) : status?.kind === "password" ? (
|
|
223
|
+
<PasswordForm
|
|
224
|
+
onSignedIn={(s) => {
|
|
225
|
+
setStatus(s);
|
|
226
|
+
onSignedIn?.(s);
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
) : signInUrl ? (
|
|
230
|
+
<Button asChild className="w-full">
|
|
231
|
+
<a href={signInUrl}>
|
|
232
|
+
<LogIn aria-hidden className="size-4" strokeWidth={1.5} />
|
|
233
|
+
Continue to sign in
|
|
234
|
+
</a>
|
|
235
|
+
</Button>
|
|
236
|
+
) : (
|
|
237
|
+
<p className="rounded-lg border border-border p-3 text-xs leading-5 text-text-color-muted">
|
|
238
|
+
Sign in through your identity provider, then reload this page. To show a sign-in button here, set G8_SIGN_IN_URL in the server environment.
|
|
239
|
+
</p>
|
|
240
|
+
)}
|
|
241
|
+
</section>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface G8AuthGateProps {
|
|
246
|
+
children: ReactNode;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Shows SignInPanel instead of the app until the visitor is signed in. Renders the app unchanged when the app has
|
|
251
|
+
* no /api/g8-auth route. The server (the proxy's `authorize`) is the real gate; this only decides what to show.
|
|
252
|
+
*/
|
|
253
|
+
export function G8AuthGate({ children }: G8AuthGateProps) {
|
|
254
|
+
const [status, setStatus] = useState<AuthStatus | null | "error" | undefined>(undefined);
|
|
255
|
+
const [retrying, setRetrying] = useState(false);
|
|
256
|
+
const titleId = useId();
|
|
257
|
+
const latest = useRef(0);
|
|
258
|
+
|
|
259
|
+
const refresh = useCallback(async () => {
|
|
260
|
+
const n = ++latest.current;
|
|
261
|
+
const next = await fetchAuthStatus();
|
|
262
|
+
if (n === latest.current) setStatus(next);
|
|
263
|
+
}, []);
|
|
264
|
+
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
void refresh();
|
|
267
|
+
const onVisible = () => {
|
|
268
|
+
if (document.visibilityState === "visible") void refresh();
|
|
269
|
+
};
|
|
270
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
271
|
+
return () => document.removeEventListener("visibilitychange", onVisible);
|
|
272
|
+
}, [refresh]);
|
|
273
|
+
|
|
274
|
+
const signOut = useCallback(async () => {
|
|
275
|
+
const current = typeof status === "object" ? status : null;
|
|
276
|
+
const out = safeUrl(current?.signOutUrl);
|
|
277
|
+
if (current?.kind !== "password") {
|
|
278
|
+
if (out) window.location.assign(out);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
await fetch(AUTH_PATH, { method: "DELETE", credentials: "same-origin" }).catch(() => undefined);
|
|
282
|
+
latest.current++;
|
|
283
|
+
setStatus({ ...current, signedIn: false });
|
|
284
|
+
}, [status]);
|
|
285
|
+
|
|
286
|
+
if (status === null) return <>{children}</>;
|
|
287
|
+
if (status === undefined) {
|
|
288
|
+
return (
|
|
289
|
+
<Frame>
|
|
290
|
+
<div role="status" className="flex flex-col items-center gap-3 text-sm text-text-color-muted">
|
|
291
|
+
<Loader2 aria-hidden className="size-5 animate-spin" />
|
|
292
|
+
Checking your session
|
|
293
|
+
</div>
|
|
294
|
+
</Frame>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (status === "error") {
|
|
298
|
+
return (
|
|
299
|
+
<Frame labelledBy={titleId}>
|
|
300
|
+
<div className="flex flex-col gap-4">
|
|
301
|
+
<BrandMark />
|
|
302
|
+
<h1 id={titleId} className="text-lg font-medium text-foreground">
|
|
303
|
+
Could not check your session
|
|
304
|
+
</h1>
|
|
305
|
+
<p className="text-sm leading-5 text-text-color-muted">This app's server did not answer. Check your connection and try again.</p>
|
|
306
|
+
<Button
|
|
307
|
+
type="button"
|
|
308
|
+
variant="outline"
|
|
309
|
+
className="w-fit"
|
|
310
|
+
disabled={retrying}
|
|
311
|
+
onClick={async () => {
|
|
312
|
+
setRetrying(true);
|
|
313
|
+
await refresh();
|
|
314
|
+
setRetrying(false);
|
|
315
|
+
}}
|
|
316
|
+
>
|
|
317
|
+
{retrying ? <Loader2 aria-hidden className="size-4 animate-spin" /> : <RefreshCw aria-hidden className="size-4" strokeWidth={1.5} />}
|
|
318
|
+
Try again
|
|
319
|
+
</Button>
|
|
320
|
+
</div>
|
|
321
|
+
</Frame>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (!status.signedIn) {
|
|
325
|
+
return (
|
|
326
|
+
<Frame>
|
|
327
|
+
<SignInPanel
|
|
328
|
+
status={status}
|
|
329
|
+
onSignedIn={(s) => {
|
|
330
|
+
latest.current++;
|
|
331
|
+
setStatus(s);
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
</Frame>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return <AuthContext.Provider value={{ status, signOut, refresh }}>{children}</AuthContext.Provider>;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Sign out, for the AppShell header. Nothing outside a signed-in G8AuthGate, or when sign-out lives at your provider without a G8_SIGN_OUT_URL. */
|
|
341
|
+
export function G8SignOutButton({ className }: { className?: string }) {
|
|
342
|
+
const auth = useG8Auth();
|
|
343
|
+
const [busy, setBusy] = useState(false);
|
|
344
|
+
if (!auth || !auth.status.signedIn) return null;
|
|
345
|
+
if (auth.status.kind !== "password" && !safeUrl(auth.status.signOutUrl)) return null;
|
|
346
|
+
return (
|
|
347
|
+
<Button
|
|
348
|
+
type="button"
|
|
349
|
+
variant="ghost"
|
|
350
|
+
size="sm"
|
|
351
|
+
className={className}
|
|
352
|
+
disabled={busy}
|
|
353
|
+
onClick={async () => {
|
|
354
|
+
setBusy(true);
|
|
355
|
+
await auth.signOut();
|
|
356
|
+
setBusy(false);
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
{busy ? <Loader2 aria-hidden className="size-4 animate-spin" /> : <LogOut aria-hidden className="size-4" strokeWidth={1.5} />}
|
|
360
|
+
Sign out
|
|
361
|
+
</Button>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
@@ -39,6 +39,13 @@ const TIER_COPY: Record<ConfirmTier, { badge: string; confirm: string; note: str
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
/** True for the tiers an app should confirm before calling. read/write never need it. */
|
|
42
|
+
/** The tier badge: ember for what cannot be undone, ochre for spend and outreach. Token classes only. */
|
|
43
|
+
const TIER_BADGE: Record<ConfirmTier, string> = {
|
|
44
|
+
billable: "bg-warning text-warning-foreground",
|
|
45
|
+
external: "bg-warning text-warning-foreground",
|
|
46
|
+
destructive: "bg-error text-error-foreground",
|
|
47
|
+
};
|
|
48
|
+
|
|
42
49
|
export function tierRequiresConfirmation(tier: string): tier is ConfirmTier {
|
|
43
50
|
return tier === "billable" || tier === "external" || tier === "destructive";
|
|
44
51
|
}
|
|
@@ -83,7 +90,7 @@ export function ConfirmSpendDialog(props: ConfirmSpendDialogProps) {
|
|
|
83
90
|
showCloseButton={false}
|
|
84
91
|
data-tier={tier}
|
|
85
92
|
style={interactive}
|
|
86
|
-
className={`max-w-md
|
|
93
|
+
className={`max-w-md gap-4 border-border bg-background p-5 shadow-xl sm:max-w-md ${props.className ?? ""}`}
|
|
87
94
|
onOpenAutoFocus={(event) => {
|
|
88
95
|
event.preventDefault();
|
|
89
96
|
openerRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
@@ -98,15 +105,15 @@ export function ConfirmSpendDialog(props: ConfirmSpendDialogProps) {
|
|
|
98
105
|
onInteractOutside={(event) => event.preventDefault()}
|
|
99
106
|
onEscapeKeyDown={(event) => confirming && event.preventDefault()}
|
|
100
107
|
>
|
|
101
|
-
<span className=
|
|
108
|
+
<span className={`inline-flex h-5 w-fit items-center rounded-full px-2 text-[11px] font-medium leading-none ${TIER_BADGE[tier]}`}>
|
|
102
109
|
{copy.badge}
|
|
103
110
|
</span>
|
|
104
|
-
<DialogTitle className="text-base font-
|
|
111
|
+
<DialogTitle className="text-base font-medium leading-6 text-foreground">{title}</DialogTitle>
|
|
105
112
|
<DialogDescription asChild>
|
|
106
|
-
<div className="space-y-2 text-sm text-
|
|
113
|
+
<div className="space-y-2 text-sm leading-5 text-muted-foreground">
|
|
107
114
|
{description ? <div>{description}</div> : null}
|
|
108
115
|
{summary.length > 0 ? (
|
|
109
|
-
<ul className="list-disc pl-5 text-
|
|
116
|
+
<ul className="list-disc space-y-0.5 pl-5 tabular-nums text-foreground">
|
|
110
117
|
{summary.map((line) => (
|
|
111
118
|
<li key={line}>{line}</li>
|
|
112
119
|
))}
|
|
@@ -115,14 +122,14 @@ export function ConfirmSpendDialog(props: ConfirmSpendDialogProps) {
|
|
|
115
122
|
<p>{copy.note}</p>
|
|
116
123
|
</div>
|
|
117
124
|
</DialogDescription>
|
|
118
|
-
<div className="flex justify-end gap-2">
|
|
125
|
+
<div className="flex justify-end gap-2 pt-1">
|
|
119
126
|
<button
|
|
120
127
|
ref={cancelRef}
|
|
121
128
|
type="button"
|
|
122
129
|
disabled={confirming}
|
|
123
130
|
onClick={onCancel}
|
|
124
131
|
style={interactive}
|
|
125
|
-
className="rounded border border-
|
|
132
|
+
className="inline-flex h-8 items-center rounded-md border border-border bg-background px-3 text-sm font-medium text-foreground transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
126
133
|
>
|
|
127
134
|
{props.cancelLabel ?? "Cancel"}
|
|
128
135
|
</button>
|
|
@@ -131,7 +138,7 @@ export function ConfirmSpendDialog(props: ConfirmSpendDialogProps) {
|
|
|
131
138
|
disabled={confirming}
|
|
132
139
|
onClick={onConfirm}
|
|
133
140
|
style={interactive}
|
|
134
|
-
className=
|
|
141
|
+
className={`inline-flex h-8 items-center rounded-md px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:opacity-50 ${tier === "destructive" ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : "bg-primary text-primary-foreground hover:bg-primary/90"}`}
|
|
135
142
|
>
|
|
136
143
|
{confirming ? "Working..." : (props.confirmLabel ?? copy.confirm)}
|
|
137
144
|
</button>
|