Skip to content
Merged
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 @@ -58,6 +58,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
'alert-dialog': dynamic(() => import('../stories/alert-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')),
otp: dynamic(() => import('../stories/otp.component.mdx')),
popover: dynamic(() => import('../stories/popover.component.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 @@ -54,6 +54,14 @@ import {
Override as IconOverride,
Sizes as IconSizes,
} from '../stories/icon.stories';
import {
BrandIcons as IconFrameBrandIcons,
CustomSurface as IconFrameCustomSurface,
Default as IconFrameDefault,
meta as iconFrameMeta,
Sizes as IconFrameSizes,
Treatments as IconFrameTreatments,
} from '../stories/icon-frame.stories';
import {
Default,
Disabled as InputDisabled,
Expand Down Expand Up @@ -123,6 +131,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 @@ -218,6 +227,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 @@ -312,6 +322,15 @@ const iconModule: StoryModule = {
Override: IconOverride,
};

const iconFrameModule: StoryModule = {
meta: iconFrameMeta,
Default: IconFrameDefault,
Sizes: IconFrameSizes,
Treatments: IconFrameTreatments,
CustomSurface: IconFrameCustomSurface,
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 @@ -487,6 +506,7 @@ export const registry: StoryModule[] = [
alertDialogComponentModule,
headingModule,
iconModule,
iconFrameModule,
menuComponentModule,
otpComponentModule,
popoverComponentModule,
Expand Down
70 changes: 70 additions & 0 deletions packages/swingset/src/stories/icon-frame.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as IconFrameStories from './icon-frame.stories';

# IconFrame

IconFrame centers an icon or other visual in a square Mosaic surface. `bordered` and `filled` are independent treatments, while `size` controls the frame dimensions. The default is a bordered 40px frame; the filled treatment uses `oklch(0.9702 0 0)` in light mode and `oklch(0.2393 0 0)` in dark mode.

## 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';

<IconFrame
bordered={false}
filled
size='lg'
>
<Icon name='check' />
</IconFrame>
```

---

## Examples

### Sizes

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

`sm`, `md`, `lg`, and `xl` map to 28px, 32px, 36px, and 40px respectively.

### Treatments

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

`bordered` and `filled` can be used separately, together, or both disabled for a centered bare icon.

### Custom surface

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

### Brand icons

<Story
name='BrandIcons'
storyModule={IconFrameStories}
/>
138 changes: 138 additions & 0 deletions packages/swingset/src/stories/icon-frame.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { IconFrameProps } from '@clerk/ui/mosaic/components/icon';
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',
styles: {
_variants: {
bordered: { true: {}, false: {} },
filled: { true: {}, false: {} },
size: { sm: {}, md: {}, lg: {}, xl: {} },
},
_defaultVariants: {
bordered: true,
filled: false,
size: 'xl',
},
},
};

function knobsAsProps(props: Record<string, unknown>) {
return props as unknown as IconFrameProps;
}

export function Default(props: Record<string, unknown>) {
return (
<IconFrame {...knobsAsProps(props)}>
<Icon name='check' />
</IconFrame>
);
}

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

export function Treatments() {
return (
<div style={{ alignItems: 'center', display: 'flex', flexWrap: 'wrap', gap: 12 }}>
<IconFrame
aria-label='Unframed'
bordered={false}
>
<Icon name='check' />
</IconFrame>
<IconFrame aria-label='Bordered'>
<Icon name='check' />
</IconFrame>
<IconFrame
aria-label='Filled'
bordered={false}
filled
>
<Icon name='check' />
</IconFrame>
<IconFrame
aria-label='Bordered and filled'
filled
>
<Icon name='check' />
</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 BrandIcons() {
return (
<div style={{ alignItems: 'center', display: 'flex', gap: 12 }}>
<IconFrame aria-label='Google'>
<ProviderLogo provider='google' />
</IconFrame>
<IconFrame aria-label='MetaMask'>
<ProviderLogo provider='metamask' />
</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