Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -52,6 +52,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 @@ -53,6 +53,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 @@ -98,6 +106,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 @@ -193,6 +202,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 @@ -288,6 +298,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 @@ -431,6 +450,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 a 6% black scrim in light mode and a 6% white scrim 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