Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tidy-icons-frame.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
1 change: 1 addition & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
icon: dynamic(() => import('../stories/icon.mdx')),
'icon-frame': dynamic(() => import('../stories/icon-frame.mdx')),
menu: dynamic(() => import('../stories/menu.component.mdx')),
popover: dynamic(() => import('../stories/popover.component.mdx')),
section: dynamic(() => import('../stories/section.mdx')),
Expand Down
20 changes: 20 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ import {
Override as IconOverride,
Sizes as IconSizes,
} from '../stories/icon.stories';
import {
BrandIcons as IconFrameBrandIcons,
CustomSurface as IconFrameCustomSurface,
Default as IconFrameDefault,
IconSizes as IconFrameIconSizes,
meta as iconFrameMeta,
MultipleTreatments as IconFrameMultipleTreatments,
} from '../stories/icon-frame.stories';
import {
Default,
Disabled as InputDisabled,
Expand Down Expand Up @@ -77,6 +85,7 @@ import {
ConnectedAccounts as SectionConnectedAccounts,
Default as SectionDefault,
Destructive as SectionDestructive,
IconFrameMedia as SectionIconFrameMedia,
meta as sectionMeta,
MultipleEmailAndPhoneNumbers as SectionMultipleEmailAndPhoneNumbers,
} from '../stories/section.stories';
Expand Down Expand Up @@ -124,6 +133,7 @@ const sectionModule: StoryModule = {
Default: SectionDefault,
MultipleEmailAndPhoneNumbers: SectionMultipleEmailAndPhoneNumbers,
ConnectedAccounts: SectionConnectedAccounts,
IconFrameMedia: SectionIconFrameMedia,
Destructive: SectionDestructive,
};
const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };
Expand Down Expand Up @@ -196,6 +206,15 @@ const iconModule: StoryModule = {
Override: IconOverride,
};

const iconFrameModule: StoryModule = {
meta: iconFrameMeta,
Default: IconFrameDefault,
IconSizes: IconFrameIconSizes,
CustomSurface: IconFrameCustomSurface,
MultipleTreatments: IconFrameMultipleTreatments,
BrandIcons: IconFrameBrandIcons,
};

// Headless primitives carry just `meta` (no story functions). Like every component
// they're documented as a single overview page; their live demos come from `<Story>` /
// `<Preview>` embeds in the MDX, which import the stories module directly.
Expand Down Expand Up @@ -263,6 +282,7 @@ export const registry: StoryModule[] = [
dialogComponentModule,
headingModule,
iconModule,
iconFrameModule,
menuComponentModule,
popoverComponentModule,
sectionModule,
Expand Down
68 changes: 68 additions & 0 deletions packages/swingset/src/stories/icon-frame.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as IconFrameStories from './icon-frame.stories';

# IconFrame

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.

## Playground

<Preview
name='Default'
storyModule={IconFrameStories}
/>

## Props

<PropTable
meta={IconFrameStories.meta}
extra={[{ name: 'render', type: '(props) => ReactNode' }]}
/>

## Usage

```tsx
import { Icon, IconFrame } from '@clerk/ui/mosaic/components/icon';
import { colorVars } from '@clerk/ui/mosaic/styles';

<IconFrame
style={{
backgroundColor: colorVars['--cl-color-primary'],
color: colorVars['--cl-color-primary-foreground'],
}}
>
<Icon name='check' size='lg' />
</IconFrame>
```

---

## Examples

### Icon sizes

<Story
name='IconSizes'
storyModule={IconFrameStories}
composition={[{ name: 'Icon', href: '/components/icon', layer: 'Components' }]}
/>

### Custom surface

<Story
name='CustomSurface'
storyModule={IconFrameStories}
/>

### Multiple treatments

<Story
name='MultipleTreatments'
storyModule={IconFrameStories}
/>

### Brand icons

<Story
name='BrandIcons'
storyModule={IconFrameStories}
/>
133 changes: 133 additions & 0 deletions packages/swingset/src/stories/icon-frame.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Icon, IconFrame } from '@clerk/ui/mosaic/components/icon';
import { colorVars, space } from '@clerk/ui/mosaic/styles';

import type { StoryMeta } from '@/lib/types';

export { default as __source } from './icon-frame.stories?raw';

const providerIconUrl = (provider: string) => `https://img.clerk.com/static/${provider}.svg`;

function ProviderLogo({ provider }: { provider: string }) {
return (
<img
alt=''
src={providerIconUrl(provider)}
style={{ display: 'block', height: space['5'], objectFit: 'contain', width: space['5'] }}
/>
);
}

export const meta: StoryMeta = {
group: 'Components',
title: 'IconFrame',
source: 'packages/ui/src/mosaic/components/icon/icon-frame.tsx',
styleEngine: 'stylex',
};

export function Default() {
return (
<IconFrame>
<Icon name='check' />
</IconFrame>
);
}

export function IconSizes() {
return (
<div style={{ alignItems: 'center', display: 'flex', gap: 12 }}>
<IconFrame>
<Icon
name='check'
size='sm'
/>
</IconFrame>
<IconFrame>
<Icon
name='check'
size='md'
/>
</IconFrame>
<IconFrame>
<Icon
name='check'
size='lg'
/>
</IconFrame>
</div>
);
}

export function CustomSurface() {
return (
<IconFrame
style={{
backgroundColor: colorVars['--cl-color-primary'],
color: colorVars['--cl-color-primary-foreground'],
}}
>
<Icon
name='check'
size='lg'
/>
</IconFrame>
);
}

export function MultipleTreatments() {
return (
<div style={{ alignItems: 'center', display: 'flex', flexWrap: 'wrap', gap: 12 }}>
<IconFrame aria-label='Default'>
<Icon name='check' />
</IconFrame>
<IconFrame
aria-label='Neutral'
style={{
backgroundColor: colorVars['--cl-color-neutral-faded'],
color: colorVars['--cl-color-neutral'],
}}
>
<Icon name='check' />
</IconFrame>
<IconFrame
aria-label='Positive'
style={{
backgroundColor: colorVars['--cl-color-positive-faded'],
color: colorVars['--cl-color-positive'],
}}
>
<Icon name='check' />
</IconFrame>
<IconFrame
aria-label='Warning'
style={{
backgroundColor: colorVars['--cl-color-warning-faded'],
color: colorVars['--cl-color-warning'],
}}
>
<Icon name='alert-circle' />
</IconFrame>
<IconFrame
aria-label='Negative'
style={{
backgroundColor: colorVars['--cl-color-negative-faded'],
color: colorVars['--cl-color-negative'],
}}
>
<Icon name='close' />
</IconFrame>
</div>
);
}

export function BrandIcons() {
return (
<div style={{ alignItems: 'center', display: 'flex', gap: 12 }}>
<IconFrame aria-label='Apple'>
<ProviderLogo provider='apple' />
</IconFrame>
<IconFrame aria-label='GitHub'>
<ProviderLogo provider='github' />
</IconFrame>
</div>
);
}
11 changes: 11 additions & 0 deletions packages/swingset/src/stories/section.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ Use `Section.Items` for a nested value list beneath a row's header item. The row
]}
/>

### Framed icon media

<Story
name='IconFrameMedia'
storyModule={SectionStories}
composition={[
{ name: 'IconFrame', href: '/components/icon-frame', layer: 'Components' },
{ name: 'Icon', href: '/components/icon', layer: 'Components' },
]}
/>

### Destructive section

<Story
Expand Down
68 changes: 39 additions & 29 deletions packages/swingset/src/stories/section.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,26 @@
import { Avatar } from '@clerk/ui/mosaic/components/avatar';
import { Badge } from '@clerk/ui/mosaic/components/badge';
import { Button } from '@clerk/ui/mosaic/components/button';
import { Icon } from '@clerk/ui/mosaic/components/icon';
import { Icon, IconFrame } from '@clerk/ui/mosaic/components/icon';
import { Section } from '@clerk/ui/mosaic/components/section';
import * as stylex from '@stylexjs/stylex';
import { space } from '@clerk/ui/mosaic/styles';

import type { StoryMeta } from '@/lib/types';

export { default as __source } from './section.stories?raw';

const providerIconUrl = (provider: string) => `https://img.clerk.com/static/${provider}.svg`;

const styles = stylex.create({
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: {
display: 'block',
height: '20px',
width: '20px',
},
});

function ProviderIcon({ provider }: { provider: string }) {
function ProviderMedia({ provider }: { provider: string }) {
return (
<Section.Media
size='lg'
{...stylex.props(styles.providerMedia)}
>
<img
alt=''
src={providerIconUrl(provider)}
{...stylex.props(styles.providerIcon)}
/>
<Section.Media size='lg'>
<IconFrame>
<img
alt=''
src={providerIconUrl(provider)}
style={{ display: 'block', height: space['5'], width: space['5'] }}
/>
</IconFrame>
</Section.Media>
);
}
Expand Down Expand Up @@ -308,7 +292,7 @@ export function ConnectedAccounts() {
<Section.Group>
<Section.Row>
<Section.Item>
<ProviderIcon provider='google' />
<ProviderMedia provider='google' />
<Section.Content>
<Section.Label>Google</Section.Label>
<Section.Description>test@google.com</Section.Description>
Expand All @@ -328,7 +312,7 @@ export function ConnectedAccounts() {
</Section.Row>
<Section.Row>
<Section.Item>
<ProviderIcon provider='apple' />
<ProviderMedia provider='apple' />
<Section.Content>
<Section.Label>Apple</Section.Label>
</Section.Content>
Expand All @@ -353,6 +337,32 @@ export function ConnectedAccounts() {
);
}

export function IconFrameMedia() {
return (
<Section.Root style={{ maxWidth: 560 }}>
<Section.Title>Team</Section.Title>
<Section.Group>
<Section.Row>
<Section.Item>
<Section.Media size='lg'>
<IconFrame>
<Icon
name='users'
size='lg'
/>
</IconFrame>
</Section.Media>
<Section.Content>
<Section.Label>Engineering</Section.Label>
<Section.Description>12 members</Section.Description>
</Section.Content>
</Section.Item>
</Section.Row>
</Section.Group>
</Section.Root>
);
}

export function Destructive() {
return (
<Section.Root>
Expand Down
Loading
Loading