Skip to content

Commit c3ce60e

Browse files
feat(ui): add SettingsGroup block
1 parent 34b1d21 commit c3ce60e

9 files changed

Lines changed: 725 additions & 0 deletions

File tree

.changeset/settings-group-block.md

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
2929
},
3030
blocks: {
3131
destructive: dynamic(() => import('../stories/destructive.mdx')),
32+
'settings-group': dynamic(() => import('../stories/settings-group.mdx')),
3233
},
3334
components: {
3435
avatar: dynamic(() => import('../stories/avatar.mdx')),

packages/swingset/src/lib/registry.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ import {
106106
ThemedScrollbar as ScrollAreaThemedScrollbar,
107107
} from '../stories/scroll-area.stories';
108108
import { meta as selectMeta } from '../stories/select.stories';
109+
import {
110+
ConnectedAccounts as SettingsGroupConnectedAccounts,
111+
Default as SettingsGroupDefault,
112+
Destructive as SettingsGroupDestructive,
113+
meta as settingsGroupMeta,
114+
} from '../stories/settings-group.stories';
109115
import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories';
110116
import { meta as tabsMeta } from '../stories/tabs.stories';
111117
import {
@@ -126,6 +132,12 @@ import { toSlug } from './slug';
126132
import type { StoryModule } from './types';
127133

128134
const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault };
135+
const settingsGroupModule: StoryModule = {
136+
meta: settingsGroupMeta,
137+
Default: SettingsGroupDefault,
138+
ConnectedAccounts: SettingsGroupConnectedAccounts,
139+
Destructive: SettingsGroupDestructive,
140+
};
129141
const organizationProfileLeaveSectionModule: StoryModule = {
130142
meta: organizationProfileLeaveSectionMeta,
131143
Default: OrganizationProfileLeaveSectionDefault,
@@ -269,6 +281,7 @@ export const registry: StoryModule[] = [
269281
organizationProfileDeleteSectionModule,
270282
// Blocks
271283
destructiveModule,
284+
settingsGroupModule,
272285
// Components
273286
avatarModule,
274287
badgeModule,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as SettingsGroupStories from './settings-group.stories';
2+
3+
# SettingsGroup
4+
5+
A settings block that carries the canonical section, surface, separator, and label/control layout. Use it for product settings rather than assembling those decisions from generic cards and item groups.
6+
7+
## Usage
8+
9+
```tsx
10+
import { SettingsGroup } from '@clerk/ui/mosaic/block/settings-group';
11+
12+
<SettingsGroup.Root title='Account'>
13+
<SettingsGroup.List>
14+
<SettingsGroup.Row>
15+
<SettingsGroup.Media>{icon}</SettingsGroup.Media>
16+
<SettingsGroup.Label description='Shown throughout the application.'>
17+
Name
18+
</SettingsGroup.Label>
19+
<SettingsGroup.Control>{control}</SettingsGroup.Control>
20+
</SettingsGroup.Row>
21+
</SettingsGroup.List>
22+
</SettingsGroup.Root>
23+
```
24+
25+
`SettingsGroup.List` owns the outlined surface and separation between direct `SettingsGroup.Row` children. `SettingsGroup.Media` reserves a consistent slot for icons, avatars, and provider marks. Each row places its label opposite its control. Use `SettingsGroup.Control` for inputs, switches, menus, and buttons—not only actions.
26+
27+
When a row contains one form control, add `field` to associate its label automatically:
28+
29+
```tsx
30+
<SettingsGroup.Row field>
31+
<SettingsGroup.Label>Name</SettingsGroup.Label>
32+
<SettingsGroup.Control>
33+
<Input />
34+
</SettingsGroup.Control>
35+
</SettingsGroup.Row>
36+
```
37+
38+
## Examples
39+
40+
<Story
41+
name='Default'
42+
storyModule={SettingsGroupStories}
43+
composition={[
44+
{ name: 'Avatar', href: '/components/avatar', layer: 'Components' },
45+
{ name: 'Input', href: '/components/input', layer: 'Components' },
46+
]}
47+
/>
48+
49+
### Connected accounts
50+
51+
<Story
52+
name='ConnectedAccounts'
53+
storyModule={SettingsGroupStories}
54+
composition={[
55+
{ name: 'Button', href: '/components/button', layer: 'Components' },
56+
{ name: 'Icon', href: '/components/icon', layer: 'Components' },
57+
]}
58+
/>
59+
60+
### Destructive setting
61+
62+
<Story
63+
name='Destructive'
64+
storyModule={SettingsGroupStories}
65+
/>
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/** @jsxImportSource @emotion/react */
2+
import { SettingsGroup } from '@clerk/ui/mosaic/block/settings-group';
3+
import { Avatar } from '@clerk/ui/mosaic/components/avatar';
4+
import { Button } from '@clerk/ui/mosaic/components/button';
5+
import { Icon } from '@clerk/ui/mosaic/components/icon';
6+
import { Input } from '@clerk/ui/mosaic/components/input';
7+
8+
import type { StoryMeta } from '@/lib/types';
9+
10+
export { default as __source } from './settings-group.stories?raw';
11+
12+
const providerIconUrl = (provider: string) => `https://img.clerk.com/static/${provider}.svg`;
13+
14+
function ProviderIcon({ provider }: { provider: string }) {
15+
return (
16+
<img
17+
alt=''
18+
src={providerIconUrl(provider)}
19+
style={{ display: 'block', height: 24, width: 24 }}
20+
/>
21+
);
22+
}
23+
24+
export const meta: StoryMeta = {
25+
group: 'Blocks',
26+
title: 'SettingsGroup',
27+
source: 'packages/ui/src/mosaic/block/settings-group.tsx',
28+
styleEngine: 'stylex',
29+
};
30+
31+
export function Default() {
32+
return (
33+
<SettingsGroup.Root
34+
title='Account'
35+
style={{ maxWidth: 560 }}
36+
>
37+
<SettingsGroup.List>
38+
<SettingsGroup.Row>
39+
<SettingsGroup.Label>Profile picture</SettingsGroup.Label>
40+
<SettingsGroup.Control>
41+
<Avatar.Root size='md'>
42+
<Avatar.Image
43+
alt='Preston Booth'
44+
src='https://avatars.githubusercontent.com/u/51144033?v=4'
45+
/>
46+
<Avatar.Fallback>PB</Avatar.Fallback>
47+
</Avatar.Root>
48+
</SettingsGroup.Control>
49+
</SettingsGroup.Row>
50+
<SettingsGroup.Row field>
51+
<SettingsGroup.Label>Name</SettingsGroup.Label>
52+
<SettingsGroup.Control>
53+
<Input
54+
defaultValue='Preston Booth'
55+
style={{ width: 176 }}
56+
/>
57+
</SettingsGroup.Control>
58+
</SettingsGroup.Row>
59+
<SettingsGroup.Row field>
60+
<SettingsGroup.Label>Username</SettingsGroup.Label>
61+
<SettingsGroup.Control>
62+
<Input
63+
defaultValue='prestonxyz'
64+
style={{ width: 176 }}
65+
/>
66+
</SettingsGroup.Control>
67+
</SettingsGroup.Row>
68+
</SettingsGroup.List>
69+
</SettingsGroup.Root>
70+
);
71+
}
72+
73+
export function ConnectedAccounts() {
74+
return (
75+
<SettingsGroup.Root
76+
title='Connected accounts'
77+
style={{ maxWidth: 560 }}
78+
>
79+
<SettingsGroup.List>
80+
<SettingsGroup.Row>
81+
<SettingsGroup.Media>
82+
<ProviderIcon provider='google' />
83+
</SettingsGroup.Media>
84+
<SettingsGroup.Label description='test@google.com'>Google</SettingsGroup.Label>
85+
<SettingsGroup.Control>
86+
<Button
87+
aria-label='Manage Google'
88+
color='neutral'
89+
shape='square'
90+
size='sm'
91+
variant='ghost'
92+
>
93+
<Icon name='ellipsis' />
94+
</Button>
95+
</SettingsGroup.Control>
96+
</SettingsGroup.Row>
97+
<SettingsGroup.Row>
98+
<SettingsGroup.Media>
99+
<ProviderIcon provider='apple' />
100+
</SettingsGroup.Media>
101+
<SettingsGroup.Label>Apple</SettingsGroup.Label>
102+
<SettingsGroup.Control>
103+
<Button
104+
color='neutral'
105+
size='sm'
106+
variant='outline'
107+
>
108+
Connect
109+
</Button>
110+
</SettingsGroup.Control>
111+
</SettingsGroup.Row>
112+
</SettingsGroup.List>
113+
</SettingsGroup.Root>
114+
);
115+
}
116+
117+
export function Destructive() {
118+
return (
119+
<SettingsGroup.Root
120+
title='Danger zone'
121+
style={{ maxWidth: 560 }}
122+
>
123+
<SettingsGroup.List>
124+
<SettingsGroup.Row>
125+
<SettingsGroup.Label description='This action is permanent and irreversible.'>
126+
Delete account
127+
</SettingsGroup.Label>
128+
<SettingsGroup.Control>
129+
<Button
130+
color='negative'
131+
size='sm'
132+
variant='outline'
133+
>
134+
Delete account
135+
</Button>
136+
</SettingsGroup.Control>
137+
</SettingsGroup.Row>
138+
</SettingsGroup.List>
139+
</SettingsGroup.Root>
140+
);
141+
}

0 commit comments

Comments
 (0)