From e4e3edc18f352482312c789c6d1118f72497c07b Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 10 Aug 2026 11:23:57 -0600 Subject: [PATCH] feat(ui): add connected accounts section --- ...user-profile-connected-accounts-section.md | 2 + .../swingset/src/components/DocsViewer.tsx | 3 + packages/swingset/src/lib/registry.ts | 9 ++ ...ser-profile-connected-accounts-section.mdx | 15 +++ ...ile-connected-accounts-section.stories.tsx | 37 ++++++++ ...rofile-connected-accounts-section.view.tsx | 94 +++++++++++++++++++ .../user-profile-profile-panel.styles.ts | 14 ++- 7 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 .changeset/user-profile-connected-accounts-section.md create mode 100644 packages/swingset/src/stories/user-profile-connected-accounts-section.mdx create mode 100644 packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx create mode 100644 packages/ui/src/mosaic/user-profile/user-profile-connected-accounts-section.view.tsx diff --git a/.changeset/user-profile-connected-accounts-section.md b/.changeset/user-profile-connected-accounts-section.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/user-profile-connected-accounts-section.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/swingset/src/components/DocsViewer.tsx b/packages/swingset/src/components/DocsViewer.tsx index 376bc86d595..73183597a61 100644 --- a/packages/swingset/src/components/DocsViewer.tsx +++ b/packages/swingset/src/components/DocsViewer.tsx @@ -13,6 +13,9 @@ const docModules: Record> = { user: { 'user-button': dynamic(() => import('../stories/user-button.mdx')), 'user-profile-account-section': dynamic(() => import('../stories/user-profile-account-section.mdx')), + 'user-profile-connected-accounts-section': dynamic( + () => import('../stories/user-profile-connected-accounts-section.mdx'), + ), }, organization: { 'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')), diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index 8ba81c94136..adebacd43ed 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -134,6 +134,10 @@ import { Default as UserProfileAccountSectionDefault, meta as userProfileAccountSectionMeta, } from '../stories/user-profile-account-section.stories'; +import { + Default as UserProfileConnectedAccountsSectionDefault, + meta as userProfileConnectedAccountsSectionMeta, +} from '../stories/user-profile-connected-accounts-section.stories'; import { toSlug } from './slug'; import type { StoryModule } from './types'; @@ -279,11 +283,16 @@ const userProfileAccountSectionModule: StoryModule = { meta: userProfileAccountSectionMeta, Default: UserProfileAccountSectionDefault, }; +const userProfileConnectedAccountsSectionModule: StoryModule = { + meta: userProfileConnectedAccountsSectionMeta, + Default: UserProfileConnectedAccountsSectionDefault, +}; export const registry: StoryModule[] = [ // User userButtonModule, userProfileAccountSectionModule, + userProfileConnectedAccountsSectionModule, // Organization organizationProfileModule, organizationProfileGeneralPanelModule, diff --git a/packages/swingset/src/stories/user-profile-connected-accounts-section.mdx b/packages/swingset/src/stories/user-profile-connected-accounts-section.mdx new file mode 100644 index 00000000000..ef3e6e1564e --- /dev/null +++ b/packages/swingset/src/stories/user-profile-connected-accounts-section.mdx @@ -0,0 +1,15 @@ +import * as Stories from './user-profile-connected-accounts-section.stories'; + +# UserProfileConnectedAccountsSection + +Connected and available external identity providers with provider-specific actions. + + diff --git a/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx b/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx new file mode 100644 index 00000000000..f2ed497579f --- /dev/null +++ b/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx @@ -0,0 +1,37 @@ +/** @jsxImportSource @emotion/react */ +import { UserProfileConnectedAccountsSectionView } from '@clerk/ui/mosaic/user-profile/user-profile-connected-accounts-section.view'; + +import type { StoryMeta } from '@/lib/types'; + +export { default as __source } from './user-profile-connected-accounts-section.stories?raw'; + +export const meta: StoryMeta = { + group: 'User', + title: 'UserProfileConnectedAccountsSection', + source: 'packages/ui/src/mosaic/user-profile/user-profile-connected-accounts-section.view.tsx', + styleEngine: 'stylex', +}; + +export function Default() { + return ( + undefined} + onManage={() => undefined} + /> + ); +} diff --git a/packages/ui/src/mosaic/user-profile/user-profile-connected-accounts-section.view.tsx b/packages/ui/src/mosaic/user-profile/user-profile-connected-accounts-section.view.tsx new file mode 100644 index 00000000000..f91bbbecfc0 --- /dev/null +++ b/packages/ui/src/mosaic/user-profile/user-profile-connected-accounts-section.view.tsx @@ -0,0 +1,94 @@ +import * as stylex from '@stylexjs/stylex'; + +import { Button } from '../components/button'; +import { Icon } from '../components/icon'; +import { Section } from '../components/section'; +import type { UserProfileMenuAction } from './user-profile-action-menu'; +import { UserProfileActionMenu } from './user-profile-action-menu'; +import { styles } from './user-profile-profile-panel.styles'; + +export interface UserProfileConnectedAccount { + id: string; + provider: string; + identifier?: string; + iconUrl?: string; + connected?: boolean; + canRemove?: boolean; +} + +export interface UserProfileConnectedAccountsSectionViewProps { + accounts: UserProfileConnectedAccount[]; + onConnect?: (id: string) => void; + onManage?: (id: string) => void; + onRemove?: (id: string) => void; +} + +export function UserProfileConnectedAccountsSectionView({ + accounts, + onConnect, + onManage, + onRemove, +}: UserProfileConnectedAccountsSectionViewProps) { + return ( + + Connected accounts + + {accounts.map(account => { + const connected = account.connected ?? Boolean(account.identifier); + const actions: UserProfileMenuAction[] = []; + if (onRemove && account.canRemove !== false) { + actions.push({ label: 'Remove', color: 'negative', onClick: () => onRemove(account.id) }); + } else if (!onRemove && onManage) { + actions.push({ label: 'Manage', onClick: () => onManage(account.id) }); + } + + return ( + + + {account.iconUrl ? ( + + + + ) : null} + + {account.provider} + {account.identifier ? {account.identifier} : null} + + + {connected ? ( + + ) : null} + {!connected && onConnect ? ( + + ) : null} + + + + ); + })} + + + ); +} diff --git a/packages/ui/src/mosaic/user-profile/user-profile-profile-panel.styles.ts b/packages/ui/src/mosaic/user-profile/user-profile-profile-panel.styles.ts index 23d0cdb4f00..74f9f68063c 100644 --- a/packages/ui/src/mosaic/user-profile/user-profile-profile-panel.styles.ts +++ b/packages/ui/src/mosaic/user-profile/user-profile-profile-panel.styles.ts @@ -9,11 +9,17 @@ export const styles = stylex.create({ display: 'flex', minWidth: 0, }, + providerMedia: { + backgroundColor: 'var(--cl-color-background)', + borderColor: 'light-dark(var(--cl-color-border-faded), var(--cl-color-background))', + borderRadius: 'var(--cl-radius-lg)', + borderStyle: 'solid', + borderWidth: '1px', + }, providerIcon: { - flexShrink: 0, - objectFit: 'contain', - height: space['6'], - width: space['6'], + display: 'block', + height: '20px', + width: '20px', }, root: { gap: space['6'],