mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
0324f3422a
commit
8a0b324dff
8 files changed
+422
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user