docs(design-system): add connect interstitials pattern (#45356)

## Summary

- Adds design-system guidance for the shared Connect interstitial layout
used by authorisation, invite, marketplace, CLI, and credit flows
- Includes a glanceable example showing the centred 400px card for
partner authorise and wrong-account invite states
- Documents Studio helpers (`InterstitialLayout`, logo helpers, account
row, `OrganizationSelector`) so future surfaces reuse one pattern
instead of bespoke shells

## Context

Most of the Studio Connect UI work from this effort has already landed.
This PR keeps the documentation and design-system example so the pattern
stays discoverable.

Related: [Shared Connect UI for Authorization and Partner
Flows](https://linear.app/supabase/project/shared-connect-ui-for-authorization-and-partner-flows-94587ac29d38)

## Test plan

- [ ] Open `/docs/ui-patterns/connect-interstitials` in the design
system
- [ ] Confirm the page appears under UI Patterns in the nav
- [ ] Confirm the example renders the authorise and wrong-account cards
side by side
- [ ] Skim the guidance for accuracy against current Studio
`InterstitialLayout` usage

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added new design-system “Connect Interstitials” example demos,
including branding variations (single vs dual logos) and a complete
connect-card flow with account row and sign-out action.
* Registered the new Connect Interstitials examples in the design-system
example registry.
* **Documentation**
* Added a “Connect Interstitials” UI Patterns page covering when to use
the pattern, recommended card/layout structure, branding/logo guidance,
and conventions for states, actions, and copy.
  * Updated the documentation sidebar to include the new page.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Danny White authored and GitHub committed 2026-07-22 00:08:32 +10:00
1 parent 0324f3422a
commit 8a0b324dff
8 files changed
+422

No files matched your search

+33
View File
@@ -2601,6 +2601,39 @@ export const Index: Record<string, any> = {
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",
+5
View File
@@ -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',
@@ -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.
<ComponentPreview
name="connect-interstitial-demo"
description="Centered 400px card with partner branding, account row, and a single primary action"
align="start"
className="p-0"
padded={false}
peekCode
wide
/>
## 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
<InterstitialLayout
logo={
<LogoPair
left={<PartnerLogo src={`${BASE_PATH}/img/icons/stripe-icon.svg`} alt="Stripe" />}
right={<SupabaseLogo />}
/>
}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="px-6 pb-6">
<InterstitialAccountRow displayName={displayName} />
<Button variant="primary" block>
Continue
</Button>
</div>
</InterstitialLayout>
```
```tsx
<InterstitialLayout
logo={<LogoPair left={<VercelLogo />} right={<SupabaseLogo />} />}
title="Connect Vercel project"
containerClassName="items-start"
cardClassName="max-w-[900px]"
>
<div className="px-6 pb-6">{projectLinker}</div>
</InterstitialLayout>
```
## 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`.
<ComponentPreview
name="connect-interstitial-logo-pair"
description="LogoPair when the user is connecting two services"
align="start"
className="p-0"
padded={false}
peekCode
wide
/>
<ComponentPreview
name="connect-interstitial-logo-single"
description="SupabaseLogo alone for first-party flows"
align="start"
className="p-0"
padded={false}
peekCode
wide
/>
```tsx
const AwsLogo = () => (
<LogoBox className="border-[#232f3e] bg-[#232f3e]">
<img alt="AWS" src={`${BASE_PATH}/img/icons/aws-icon.svg`} className="w-8" />
</LogoBox>
)
<LogoPair left={<AwsLogo />} right={<SupabaseLogo />} />
```
## Account row
Use `InterstitialAccountRow` for signed-in context. Do not recreate it locally.
```tsx
<InterstitialAccountRow avatarUrl={avatarUrl} displayName={displayName} action={signOutButton} />
```
## Organisation selection
Use `OrganizationSelector` when the flow needs an organisation pick. Extend it
for new states instead of inventing a parallel card style.
```tsx
<OrganizationSelector
organizations={linkableOrganizations}
selectedSlug={selectedOrgSlug}
onSelect={setSelectedOrgSlug}
createLabel="Create new organization"
onCreate={() => 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.
@@ -0,0 +1,30 @@
import { Button } from 'ui'
import {
AccountRow,
InterstitialShell,
LogoPair,
SignOutButton,
StripeLogo,
SupabaseLogo,
} from './connect-interstitial-shared'
export default function ConnectInterstitialDemo() {
return (
<InterstitialShell
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
<Button variant="primary" block>
Authorize Stripe Projects
</Button>
<Button variant="text" block>
Cancel
</Button>
</div>
</InterstitialShell>
)
}
@@ -0,0 +1,27 @@
import { Button } from 'ui'
import {
AccountRow,
InterstitialShell,
LogoPair,
SignOutButton,
StripeLogo,
SupabaseLogo,
} from './connect-interstitial-shared'
export default function ConnectInterstitialLogoPair() {
return (
<InterstitialShell
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
<Button variant="primary" block>
Authorize Stripe Projects
</Button>
</div>
</InterstitialShell>
)
}
@@ -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 (
<InterstitialShell
logo={<SupabaseLogo />}
title="Join organization"
description="You have been invited to Acme Labs"
>
<div className="flex flex-col gap-4">
<Admonition
type="warning"
title="Wrong account"
description="Sign in with the Supabase account that received this invite, then open the link again."
/>
<AccountRow displayName="alex@example.com" />
<Button variant="primary" block>
Sign out and continue
</Button>
</div>
</InterstitialShell>
)
}
@@ -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 (
<div
className={cn(
'flex size-12 items-center justify-center overflow-hidden rounded-xl border bg-muted',
className
)}
>
{children}
</div>
)
}
export function LogoPair({ left, right }: { left: React.ReactNode; right: React.ReactNode }) {
return (
<div className="flex items-center justify-center gap-2.5">
{left}
<ArrowRightLeft className="size-4 text-foreground-muted" />
{right}
</div>
)
}
export function StripeLogo() {
return (
<LogoBox className="border-[#533afd] bg-[#533afd]">
<svg viewBox="0 0 512 512" className="size-full" aria-hidden>
<path
fill="#fff"
fillRule="evenodd"
d="m132 380 248-52.593V132l-248 53.208z"
clipRule="evenodd"
/>
</svg>
</LogoBox>
)
}
export function SupabaseLogo() {
return (
<LogoBox className="bg-surface-75">
<svg viewBox="0 0 109 113" className="size-7" aria-hidden>
<path
d="M63.708 110.284c-2.86 3.601-8.658 1.628-8.727-2.97L53.974 40.063h45.22c8.19 0 12.758 9.46 7.665 15.874L63.708 110.284Z"
fill="#3ECF8E"
/>
<path
d="M45.317 2.071c2.86-3.601 8.658-1.628 8.726 2.97l.442 67.251H9.831C1.64 72.292-2.928 62.832 2.166 56.418L45.317 2.071Z"
fill="#3ECF8E"
/>
</svg>
</LogoBox>
)
}
export function AccountRow({
displayName,
action,
}: {
displayName: string
action?: React.ReactNode
}) {
return (
<Card className={cn('shadow-none', !action && 'border-muted bg-surface-200/50')}>
<CardContent
className={cn('flex gap-3 border-none', action ? 'items-center px-4 py-3' : 'p-3')}
>
<Avatar className="size-8 border border-muted">
<AvatarFallback className="text-xs">A</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="text-xs text-foreground-light">Signed in as</p>
<p className="truncate text-sm text-foreground">{displayName}</p>
</div>
{action}
</CardContent>
</Card>
)
}
export function InterstitialShell({
logo,
title,
description,
children,
}: {
logo: React.ReactNode
title: string
description?: string
children: React.ReactNode
}) {
return (
<div className="flex min-h-[520px] w-full items-center justify-center bg-studio px-2 py-6">
<Card className="w-full max-w-[400px] overflow-hidden">
<CardHeader className="items-center gap-0 space-y-0 border-0 px-6 py-6 text-center">
<div className="mb-4 flex justify-center">{logo}</div>
<div className="flex flex-col items-center gap-1">
<h1 className="text-balance text-lg font-medium tracking-tight text-foreground">
{title}
</h1>
{description ? (
<p className="m-0 px-3 text-balance text-sm leading-tight text-foreground-lighter">
{description}
</p>
) : null}
</div>
</CardHeader>
<div className="px-6 pb-6">{children}</div>
</Card>
</div>
)
}
export function SignOutButton() {
return <Button variant="default" icon={<LogOut />} className="px-2" aria-label="Sign out" />
}
+15
View File
@@ -1398,6 +1398,21 @@ export const examples: Registry = [
type: 'components:example',
files: ['example/page-layout-settings.tsx'],
},
{
name: 'connect-interstitial-demo',
type: 'components:example',
files: ['example/connect-interstitial-demo.tsx'],
},
{
name: 'connect-interstitial-logo-pair',
type: 'components:example',
files: ['example/connect-interstitial-logo-pair.tsx'],
},
{
name: 'connect-interstitial-logo-single',
type: 'components:example',
files: ['example/connect-interstitial-logo-single.tsx'],
},
{
name: 'page-layout-auth-emails',
type: 'components:example',