diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index 12c8f4c965b..84d94abdbc4 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -2601,6 +2601,39 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "connect-interstitial-demo": { + name: "connect-interstitial-demo", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/connect-interstitial-demo")), + source: "", + files: ["registry/default/example/connect-interstitial-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "connect-interstitial-logo-pair": { + name: "connect-interstitial-logo-pair", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-pair")), + source: "", + files: ["registry/default/example/connect-interstitial-logo-pair.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "connect-interstitial-logo-single": { + name: "connect-interstitial-logo-single", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-single")), + source: "", + files: ["registry/default/example/connect-interstitial-logo-single.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "page-layout-auth-emails": { name: "page-layout-auth-emails", type: "components:example", diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts index 6e72373cf39..df52dd81aa1 100644 --- a/apps/design-system/config/docs.ts +++ b/apps/design-system/config/docs.ts @@ -70,6 +70,11 @@ export const docsConfig: DocsConfig = { href: '/docs/ui-patterns/charts', items: [], }, + { + title: 'Connect Interstitials', + href: '/docs/ui-patterns/connect-interstitials', + items: [], + }, { title: 'Empty States', href: '/docs/ui-patterns/empty-states', diff --git a/apps/design-system/content/docs/ui-patterns/connect-interstitials.mdx b/apps/design-system/content/docs/ui-patterns/connect-interstitials.mdx new file mode 100644 index 00000000000..b5f1514af02 --- /dev/null +++ b/apps/design-system/content/docs/ui-patterns/connect-interstitials.mdx @@ -0,0 +1,161 @@ +--- +title: Connect Interstitials +description: Shared layout guidance for focused authorisation, invite, marketplace, CLI, and credit redemption flows. +--- + +Connect interstitials are focused, single-card flows that sit outside the main +Studio shell. Use the shared `InterstitialLayout` family instead of building +bespoke centered cards, logos, account rows, or organisation selectors. + + + +## Use this pattern for + +This pattern fits short-lived connect flows: partner authorisation and consent +(OAuth, MCP, Stripe Projects), organisation invites, marketplace and billing +connections (AWS Marketplace, Vercel install, credit redemption), and CLI or +device-code sign-in. Use the same shell for their loading, error, success, and +wrong-account states. + +Do not use it for normal authenticated Studio pages. Those should use the +standard [page layout](./layout) patterns. + +## Source of truth + +```tsx +import { OrganizationSelector } from '@/components/interfaces/Connect/OrganizationSelector' +import { + InterstitialAccountRow, + InterstitialLayout, + LogoBox, + LogoPair, + PartnerLogo, + SupabaseLogo, +} from '@/components/layouts/InterstitialLayout' +``` + +## Basic shape + +Use `InterstitialLayout` for the outer card, then put route-specific content in +`px-6 pb-6`. Widen the card only when the flow embeds a real tool, such as +project linking. + +```tsx +} + right={} + /> + } + title="Authorize Stripe Projects" + description="This will create an organization on your behalf in Supabase" +> +
+ + +
+
+``` + +```tsx +} right={} />} + title="Connect Vercel project" + containerClassName="items-start" + cardClassName="max-w-[900px]" +> +
{projectLinker}
+
+``` + +## Logos + +Use `LogoPair` when the user is connecting two services, and `SupabaseLogo` +alone for first-party flows. `PartnerLogo` fills the 48px box edge-to-edge; +`LogoBox` is for custom inset marks or logos that need their own background. +Store new partner icons in `apps/studio/public/img/icons`. + + + + + +```tsx +const AwsLogo = () => ( + + AWS + +) + +} right={} /> +``` + +## Account row + +Use `InterstitialAccountRow` for signed-in context. Do not recreate it locally. + +```tsx + +``` + +## Organisation selection + +Use `OrganizationSelector` when the flow needs an organisation pick. Extend it +for new states instead of inventing a parallel card style. + +```tsx + setShowOrgCreationDialog(true)} +/> +``` + +## Actions + +Prefer one full-width primary action. A full-width text button is fine for a +secondary action that still belongs in the flow. + +## States + +Keep loading, invalid, error, and success states inside the same card when the +route can explain them. Use `ShimmeringLoader` for loading, and `Admonition` +for warning, error, note, and success copy. + +## Copy + +Use sentence case. Prefer `sign in` over `login`. Titles and primary actions +should follow `Verb -> Thing`, for example `Authorize Stripe Projects` or +`Install Vercel`. + +Keep the layout title static across states and put state-specific copy in the +body. Header descriptions should stay short and should not end with a full +stop. diff --git a/apps/design-system/registry/default/example/connect-interstitial-demo.tsx b/apps/design-system/registry/default/example/connect-interstitial-demo.tsx new file mode 100644 index 00000000000..4bee00df445 --- /dev/null +++ b/apps/design-system/registry/default/example/connect-interstitial-demo.tsx @@ -0,0 +1,30 @@ +import { Button } from 'ui' + +import { + AccountRow, + InterstitialShell, + LogoPair, + SignOutButton, + StripeLogo, + SupabaseLogo, +} from './connect-interstitial-shared' + +export default function ConnectInterstitialDemo() { + return ( + } right={} />} + title="Authorize Stripe Projects" + description="This will create an organization on your behalf in Supabase" + > +
+ } /> + + +
+
+ ) +} diff --git a/apps/design-system/registry/default/example/connect-interstitial-logo-pair.tsx b/apps/design-system/registry/default/example/connect-interstitial-logo-pair.tsx new file mode 100644 index 00000000000..a75e0d1f653 --- /dev/null +++ b/apps/design-system/registry/default/example/connect-interstitial-logo-pair.tsx @@ -0,0 +1,27 @@ +import { Button } from 'ui' + +import { + AccountRow, + InterstitialShell, + LogoPair, + SignOutButton, + StripeLogo, + SupabaseLogo, +} from './connect-interstitial-shared' + +export default function ConnectInterstitialLogoPair() { + return ( + } right={} />} + title="Authorize Stripe Projects" + description="This will create an organization on your behalf in Supabase" + > +
+ } /> + +
+
+ ) +} diff --git a/apps/design-system/registry/default/example/connect-interstitial-logo-single.tsx b/apps/design-system/registry/default/example/connect-interstitial-logo-single.tsx new file mode 100644 index 00000000000..7e8fb7b0c5a --- /dev/null +++ b/apps/design-system/registry/default/example/connect-interstitial-logo-single.tsx @@ -0,0 +1,26 @@ +import { Button } from 'ui' +import { Admonition } from 'ui-patterns/admonition' + +import { AccountRow, InterstitialShell, SupabaseLogo } from './connect-interstitial-shared' + +export default function ConnectInterstitialLogoSingle() { + return ( + } + title="Join organization" + description="You have been invited to Acme Labs" + > +
+ + + +
+
+ ) +} diff --git a/apps/design-system/registry/default/example/connect-interstitial-shared.tsx b/apps/design-system/registry/default/example/connect-interstitial-shared.tsx new file mode 100644 index 00000000000..24c35e4da08 --- /dev/null +++ b/apps/design-system/registry/default/example/connect-interstitial-shared.tsx @@ -0,0 +1,125 @@ +import { ArrowRightLeft, LogOut } from 'lucide-react' +import { Avatar, AvatarFallback, Button, Card, CardContent, CardHeader, cn } from 'ui' + +export function LogoBox({ + children, + className, +}: { + children: React.ReactNode + className?: string +}) { + return ( +
+ {children} +
+ ) +} + +export function LogoPair({ left, right }: { left: React.ReactNode; right: React.ReactNode }) { + return ( +
+ {left} + + {right} +
+ ) +} + +export function StripeLogo() { + return ( + + + + + + ) +} + +export function SupabaseLogo() { + return ( + + + + + + + ) +} + +export function AccountRow({ + displayName, + action, +}: { + displayName: string + action?: React.ReactNode +}) { + return ( + + + + A + +
+

Signed in as

+

{displayName}

+
+ {action} +
+
+ ) +} + +export function InterstitialShell({ + logo, + title, + description, + children, +}: { + logo: React.ReactNode + title: string + description?: string + children: React.ReactNode +}) { + return ( +
+ + +
{logo}
+
+

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+
+
{children}
+
+
+ ) +} + +export function SignOutButton() { + return