Skip to content

Commit 39e36de

Browse files
feat(ui): add Mosaic IconFrame
1 parent 75ec072 commit 39e36de

12 files changed

Lines changed: 380 additions & 4 deletions

File tree

.changeset/tidy-icons-frame.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
@@ -30,6 +30,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
3030
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
3131
heading: dynamic(() => import('../stories/heading.mdx')),
3232
icon: dynamic(() => import('../stories/icon.mdx')),
33+
'icon-frame': dynamic(() => import('../stories/icon-frame.mdx')),
3334
menu: dynamic(() => import('../stories/menu.component.mdx')),
3435
popover: dynamic(() => import('../stories/popover.component.mdx')),
3536
section: dynamic(() => import('../stories/section.mdx')),

packages/swingset/src/lib/registry.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ import {
4040
Override as IconOverride,
4141
Sizes as IconSizes,
4242
} from '../stories/icon.stories';
43+
import {
44+
BrandIcons as IconFrameBrandIcons,
45+
CustomSurface as IconFrameCustomSurface,
46+
Default as IconFrameDefault,
47+
IconSizes as IconFrameIconSizes,
48+
meta as iconFrameMeta,
49+
MultipleTreatments as IconFrameMultipleTreatments,
50+
} from '../stories/icon-frame.stories';
4351
import {
4452
Default,
4553
Disabled as InputDisabled,
@@ -77,6 +85,7 @@ import {
7785
ConnectedAccounts as SectionConnectedAccounts,
7886
Default as SectionDefault,
7987
Destructive as SectionDestructive,
88+
IconFrameMedia as SectionIconFrameMedia,
8089
meta as sectionMeta,
8190
MultipleEmailAndPhoneNumbers as SectionMultipleEmailAndPhoneNumbers,
8291
} from '../stories/section.stories';
@@ -124,6 +133,7 @@ const sectionModule: StoryModule = {
124133
Default: SectionDefault,
125134
MultipleEmailAndPhoneNumbers: SectionMultipleEmailAndPhoneNumbers,
126135
ConnectedAccounts: SectionConnectedAccounts,
136+
IconFrameMedia: SectionIconFrameMedia,
127137
Destructive: SectionDestructive,
128138
};
129139
const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };
@@ -196,6 +206,15 @@ const iconModule: StoryModule = {
196206
Override: IconOverride,
197207
};
198208

209+
const iconFrameModule: StoryModule = {
210+
meta: iconFrameMeta,
211+
Default: IconFrameDefault,
212+
IconSizes: IconFrameIconSizes,
213+
CustomSurface: IconFrameCustomSurface,
214+
MultipleTreatments: IconFrameMultipleTreatments,
215+
BrandIcons: IconFrameBrandIcons,
216+
};
217+
199218
// Headless primitives carry just `meta` (no story functions). Like every component
200219
// they're documented as a single overview page; their live demos come from `<Story>` /
201220
// `<Preview>` embeds in the MDX, which import the stories module directly.
@@ -263,6 +282,7 @@ export const registry: StoryModule[] = [
263282
dialogComponentModule,
264283
headingModule,
265284
iconModule,
285+
iconFrameModule,
266286
menuComponentModule,
267287
popoverComponentModule,
268288
sectionModule,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as IconFrameStories from './icon-frame.stories';
2+
3+
# IconFrame
4+
5+
IconFrame places an icon or other visual in a square Mosaic surface. The child owns its size and artwork, while the frame adds padding, border, radius, centering, and a default background. Customize the surface through native `style` or `className` overrides.
6+
7+
## Playground
8+
9+
<Preview
10+
name='Default'
11+
storyModule={IconFrameStories}
12+
/>
13+
14+
## Props
15+
16+
<PropTable
17+
meta={IconFrameStories.meta}
18+
extra={[{ name: 'render', type: '(props) => ReactNode' }]}
19+
/>
20+
21+
## Usage
22+
23+
```tsx
24+
import { Icon, IconFrame } from '@clerk/ui/mosaic/components/icon';
25+
import { colorVars } from '@clerk/ui/mosaic/styles';
26+
27+
<IconFrame
28+
style={{
29+
backgroundColor: colorVars['--cl-color-primary'],
30+
color: colorVars['--cl-color-primary-foreground'],
31+
}}
32+
>
33+
<Icon name='check' size='lg' />
34+
</IconFrame>
35+
```
36+
37+
---
38+
39+
## Examples
40+
41+
### Icon sizes
42+
43+
<Story
44+
name='IconSizes'
45+
storyModule={IconFrameStories}
46+
composition={[{ name: 'Icon', href: '/components/icon', layer: 'Components' }]}
47+
/>
48+
49+
### Custom surface
50+
51+
<Story
52+
name='CustomSurface'
53+
storyModule={IconFrameStories}
54+
/>
55+
56+
### Multiple treatments
57+
58+
<Story
59+
name='MultipleTreatments'
60+
storyModule={IconFrameStories}
61+
/>
62+
63+
### Brand icons
64+
65+
<Story
66+
name='BrandIcons'
67+
storyModule={IconFrameStories}
68+
/>
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Icon, IconFrame } from '@clerk/ui/mosaic/components/icon';
2+
import { colorVars } from '@clerk/ui/mosaic/styles';
3+
4+
import type { StoryMeta } from '@/lib/types';
5+
6+
export { default as __source } from './icon-frame.stories?raw';
7+
8+
const providerIconUrl = (provider: string) => `https://img.clerk.com/static/${provider}.svg`;
9+
10+
function ProviderIcon({ provider }: { provider: string }) {
11+
return (
12+
<img
13+
alt=''
14+
src={providerIconUrl(provider)}
15+
style={{ display: 'block', height: 20, objectFit: 'contain', width: 20 }}
16+
/>
17+
);
18+
}
19+
20+
export const meta: StoryMeta = {
21+
group: 'Components',
22+
title: 'IconFrame',
23+
source: 'packages/ui/src/mosaic/components/icon/icon-frame.tsx',
24+
styleEngine: 'stylex',
25+
};
26+
27+
export function Default() {
28+
return (
29+
<IconFrame>
30+
<Icon name='check' />
31+
</IconFrame>
32+
);
33+
}
34+
35+
export function IconSizes() {
36+
return (
37+
<div style={{ alignItems: 'center', display: 'flex', gap: 12 }}>
38+
<IconFrame>
39+
<Icon
40+
name='check'
41+
size='sm'
42+
/>
43+
</IconFrame>
44+
<IconFrame>
45+
<Icon
46+
name='check'
47+
size='md'
48+
/>
49+
</IconFrame>
50+
<IconFrame>
51+
<Icon
52+
name='check'
53+
size='lg'
54+
/>
55+
</IconFrame>
56+
</div>
57+
);
58+
}
59+
60+
export function CustomSurface() {
61+
return (
62+
<IconFrame
63+
style={{
64+
backgroundColor: colorVars['--cl-color-primary'],
65+
color: colorVars['--cl-color-primary-foreground'],
66+
}}
67+
>
68+
<Icon
69+
name='check'
70+
size='lg'
71+
/>
72+
</IconFrame>
73+
);
74+
}
75+
76+
export function MultipleTreatments() {
77+
return (
78+
<div style={{ alignItems: 'center', display: 'flex', flexWrap: 'wrap', gap: 12 }}>
79+
<IconFrame aria-label='Default'>
80+
<Icon name='check' />
81+
</IconFrame>
82+
<IconFrame
83+
aria-label='Neutral'
84+
style={{
85+
backgroundColor: colorVars['--cl-color-neutral-faded'],
86+
color: colorVars['--cl-color-neutral'],
87+
}}
88+
>
89+
<Icon name='check' />
90+
</IconFrame>
91+
<IconFrame
92+
aria-label='Positive'
93+
style={{
94+
backgroundColor: colorVars['--cl-color-positive-faded'],
95+
color: colorVars['--cl-color-positive'],
96+
}}
97+
>
98+
<Icon name='check' />
99+
</IconFrame>
100+
<IconFrame
101+
aria-label='Warning'
102+
style={{
103+
backgroundColor: colorVars['--cl-color-warning-faded'],
104+
color: colorVars['--cl-color-warning'],
105+
}}
106+
>
107+
<Icon name='alert-circle' />
108+
</IconFrame>
109+
<IconFrame
110+
aria-label='Negative'
111+
style={{
112+
backgroundColor: colorVars['--cl-color-negative-faded'],
113+
color: colorVars['--cl-color-negative'],
114+
}}
115+
>
116+
<Icon name='close' />
117+
</IconFrame>
118+
</div>
119+
);
120+
}
121+
122+
export function BrandIcons() {
123+
return (
124+
<div style={{ alignItems: 'center', display: 'flex', gap: 12 }}>
125+
<IconFrame aria-label='Apple'>
126+
<ProviderIcon provider='apple' />
127+
</IconFrame>
128+
<IconFrame aria-label='GitHub'>
129+
<ProviderIcon provider='github' />
130+
</IconFrame>
131+
</div>
132+
);
133+
}

packages/swingset/src/stories/section.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ Use `Section.Items` for a nested value list beneath a row's header item. The row
6464
]}
6565
/>
6666

67+
### Framed icon media
68+
69+
<Story
70+
name='IconFrameMedia'
71+
storyModule={SectionStories}
72+
composition={[
73+
{ name: 'IconFrame', href: '/components/icon-frame', layer: 'Components' },
74+
{ name: 'Icon', href: '/components/icon', layer: 'Components' },
75+
]}
76+
/>
77+
6778
### Destructive section
6879

6980
<Story

packages/swingset/src/stories/section.stories.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Avatar } from '@clerk/ui/mosaic/components/avatar';
22
import { Badge } from '@clerk/ui/mosaic/components/badge';
33
import { Button } from '@clerk/ui/mosaic/components/button';
4-
import { Icon } from '@clerk/ui/mosaic/components/icon';
4+
import { Icon, IconFrame } from '@clerk/ui/mosaic/components/icon';
55
import { Section } from '@clerk/ui/mosaic/components/section';
66
import * as stylex from '@stylexjs/stylex';
77

@@ -353,6 +353,32 @@ export function ConnectedAccounts() {
353353
);
354354
}
355355

356+
export function IconFrameMedia() {
357+
return (
358+
<Section.Root style={{ maxWidth: 560 }}>
359+
<Section.Title>Team</Section.Title>
360+
<Section.Group>
361+
<Section.Row>
362+
<Section.Item>
363+
<Section.Media size='lg'>
364+
<IconFrame>
365+
<Icon
366+
name='users'
367+
size='lg'
368+
/>
369+
</IconFrame>
370+
</Section.Media>
371+
<Section.Content>
372+
<Section.Label>Engineering</Section.Label>
373+
<Section.Description>12 members</Section.Description>
374+
</Section.Content>
375+
</Section.Item>
376+
</Section.Row>
377+
</Section.Group>
378+
</Section.Root>
379+
);
380+
}
381+
356382
export function Destructive() {
357383
return (
358384
<Section.Root>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as stylex from '@stylexjs/stylex';
2+
3+
import { colorVars, radiusVars, space } from '../../tokens.stylex';
4+
5+
export const styles = stylex.create({
6+
base: {
7+
borderColor: colorVars['--cl-color-border'],
8+
borderRadius: radiusVars['--cl-radius-md'],
9+
borderStyle: 'solid',
10+
borderWidth: '1px',
11+
overflow: 'hidden',
12+
alignItems: 'center',
13+
aspectRatio: '1 / 1',
14+
backgroundColor: colorVars['--cl-color-background'],
15+
display: 'inline-flex',
16+
flexShrink: 0,
17+
justifyContent: 'center',
18+
height: space['10'],
19+
},
20+
});

0 commit comments

Comments
 (0)