Skip to content

Commit 303236e

Browse files
feat(ui): add connected accounts section
1 parent cc2748c commit 303236e

7 files changed

Lines changed: 165 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

packages/swingset/src/components/DocsViewer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
1313
user: {
1414
'user-button': dynamic(() => import('../stories/user-button.mdx')),
1515
'user-profile-account-section': dynamic(() => import('../stories/user-profile-account-section.mdx')),
16+
'user-profile-connected-accounts-section': dynamic(
17+
() => import('../stories/user-profile-connected-accounts-section.mdx'),
18+
),
1619
},
1720
organization: {
1821
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),

packages/swingset/src/lib/registry.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ import {
134134
Default as UserProfileAccountSectionDefault,
135135
meta as userProfileAccountSectionMeta,
136136
} from '../stories/user-profile-account-section.stories';
137+
import {
138+
Default as UserProfileConnectedAccountsSectionDefault,
139+
meta as userProfileConnectedAccountsSectionMeta,
140+
} from '../stories/user-profile-connected-accounts-section.stories';
137141
import { toSlug } from './slug';
138142
import type { StoryModule } from './types';
139143

@@ -279,11 +283,16 @@ const userProfileAccountSectionModule: StoryModule = {
279283
meta: userProfileAccountSectionMeta,
280284
Default: UserProfileAccountSectionDefault,
281285
};
286+
const userProfileConnectedAccountsSectionModule: StoryModule = {
287+
meta: userProfileConnectedAccountsSectionMeta,
288+
Default: UserProfileConnectedAccountsSectionDefault,
289+
};
282290

283291
export const registry: StoryModule[] = [
284292
// User
285293
userButtonModule,
286294
userProfileAccountSectionModule,
295+
userProfileConnectedAccountsSectionModule,
287296
// Organization
288297
organizationProfileModule,
289298
organizationProfileGeneralPanelModule,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Stories from './user-profile-connected-accounts-section.stories';
2+
3+
# UserProfileConnectedAccountsSection
4+
5+
Connected and available external identity providers with provider-specific actions.
6+
7+
<Story
8+
name='Default'
9+
storyModule={Stories}
10+
composition={[
11+
{ name: 'Settings', href: '/components/settings', layer: 'Components' },
12+
{ name: 'Button', href: '/components/button', layer: 'Components' },
13+
{ name: 'Icon', href: '/components/icon', layer: 'Components' },
14+
]}
15+
/>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/** @jsxImportSource @emotion/react */
2+
import { UserProfileConnectedAccountsSectionView } from '@clerk/ui/mosaic/user-profile/user-profile-connected-accounts-section.view';
3+
4+
import type { StoryMeta } from '@/lib/types';
5+
6+
export { default as __source } from './user-profile-connected-accounts-section.stories?raw';
7+
8+
export const meta: StoryMeta = {
9+
group: 'User',
10+
title: 'UserProfileConnectedAccountsSection',
11+
source: 'packages/ui/src/mosaic/user-profile/user-profile-connected-accounts-section.view.tsx',
12+
styleEngine: 'stylex',
13+
};
14+
15+
export function Default() {
16+
return (
17+
<UserProfileConnectedAccountsSectionView
18+
accounts={[
19+
{
20+
id: 'google',
21+
provider: 'Google',
22+
identifier: 'test@google.com',
23+
iconUrl: 'https://img.clerk.com/static/google.svg',
24+
connected: true,
25+
},
26+
{
27+
id: 'apple',
28+
provider: 'Apple',
29+
iconUrl: 'https://img.clerk.com/static/apple.svg',
30+
connected: false,
31+
},
32+
]}
33+
onConnect={() => undefined}
34+
onManage={() => undefined}
35+
/>
36+
);
37+
}

packages/ui/src/mosaic/icons/registry.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ const Plus = glyph(
9494
/>,
9595
);
9696

97+
const ArrowRightTop = glyph(
98+
<path
99+
d='M6.35014 5.40727L10.8285 5.17157M10.8285 5.17157L10.5928 9.64991M10.8285 5.17157L5.17163 10.8284'
100+
{...strokeProps}
101+
/>,
102+
);
103+
97104
const Pen = glyph(
98105
<path
99106
d='M12.03 3.972a1.59 1.59 0 0 0-2.261 0l-.01.01-5.056 4.89a1.25 1.25 0 0 0-.351.627l-.61 2.747 2.542-.598a1.25 1.25 0 0 0 .59-.325L12.085 6.2c.573-.638.549-1.62-.057-2.228M8.71 2.909a3.09 3.09 0 0 1 4.383.005 3.126 3.126 0 0 1 .06 4.341l-5.228 5.138a2.75 2.75 0 0 1-1.298.715l-3.705.872a.75.75 0 0 1-.904-.893l.87-3.914a2.75 2.75 0 0 1 .772-1.38z'
@@ -142,6 +149,7 @@ const AlertCircle = glyph(
142149
/** Runtime name → glyph map. `Icon`'s `name` prop is typed from these keys. */
143150
export const iconRegistry = {
144151
'alert-circle': AlertCircle,
152+
'arrow-right-top': ArrowRightTop,
145153
'chevron-right': ChevronRight,
146154
'chevron-left': ChevronLeft,
147155
'chevron-down': ChevronDown,
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as stylex from '@stylexjs/stylex';
2+
3+
import { Button } from '../components/button';
4+
import { Icon } from '../components/icon';
5+
import { Settings } from '../components/settings';
6+
import type { UserProfileMenuAction } from './user-profile-action-menu';
7+
import { UserProfileActionMenu } from './user-profile-action-menu';
8+
import { styles } from './user-profile-profile-panel.styles';
9+
10+
export interface UserProfileConnectedAccount {
11+
id: string;
12+
provider: string;
13+
identifier?: string;
14+
iconUrl?: string;
15+
connected?: boolean;
16+
canRemove?: boolean;
17+
}
18+
19+
export interface UserProfileConnectedAccountsSectionViewProps {
20+
accounts: UserProfileConnectedAccount[];
21+
onConnect?: (id: string) => void;
22+
onManage?: (id: string) => void;
23+
onRemove?: (id: string) => void;
24+
}
25+
26+
export function UserProfileConnectedAccountsSectionView({
27+
accounts,
28+
onConnect,
29+
onManage,
30+
onRemove,
31+
}: UserProfileConnectedAccountsSectionViewProps) {
32+
return (
33+
<Settings.Root>
34+
<Settings.Title>Connected accounts</Settings.Title>
35+
<Settings.Group>
36+
{accounts.map(account => {
37+
const connected = account.connected ?? Boolean(account.identifier);
38+
const actions: UserProfileMenuAction[] = [];
39+
if (onRemove && account.canRemove !== false) {
40+
actions.push({ label: 'Remove', color: 'negative', onClick: () => onRemove(account.id) });
41+
} else if (!onRemove && onManage) {
42+
actions.push({ label: 'Manage', onClick: () => onManage(account.id) });
43+
}
44+
45+
return (
46+
<Settings.Row key={account.id}>
47+
<Settings.Item>
48+
{account.iconUrl ? (
49+
<Settings.Media>
50+
<img
51+
alt=''
52+
src={account.iconUrl}
53+
{...stylex.props(styles.providerIcon)}
54+
/>
55+
</Settings.Media>
56+
) : null}
57+
<Settings.Content>
58+
<Settings.Label>{account.provider}</Settings.Label>
59+
{account.identifier ? <Settings.Description>{account.identifier}</Settings.Description> : null}
60+
</Settings.Content>
61+
<Settings.Actions>
62+
{connected ? (
63+
<UserProfileActionMenu
64+
actions={actions}
65+
label={`Manage ${account.provider}`}
66+
/>
67+
) : null}
68+
{!connected && onConnect ? (
69+
<Button
70+
color='neutral'
71+
size='sm'
72+
variant='outline'
73+
onClick={() => onConnect(account.id)}
74+
>
75+
Connect
76+
<Icon
77+
name='arrow-right-top'
78+
placement='inline-end'
79+
size='sm'
80+
/>
81+
</Button>
82+
) : null}
83+
</Settings.Actions>
84+
</Settings.Item>
85+
</Settings.Row>
86+
);
87+
})}
88+
</Settings.Group>
89+
</Settings.Root>
90+
);
91+
}

0 commit comments

Comments
 (0)