Skip to content
Open
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/mosaic-item.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 @@ -31,6 +31,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
button: dynamic(() => import('../stories/button.mdx')),
card: dynamic(() => import('../stories/card.component.mdx')),
input: dynamic(() => import('../stories/input.mdx')),
item: dynamic(() => import('../stories/item.mdx')),
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
icon: dynamic(() => import('../stories/icon.mdx')),
Expand Down
16 changes: 16 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ import {
meta as inputMeta,
Sizes as InputSizes,
} from '../stories/input.stories';
import {
Default as ItemDefault,
Group as ItemGroup,
Interactive as ItemInteractive,
meta as itemMeta,
Variants as ItemVariants,
} from '../stories/item.stories';
import { meta as menuMeta } from '../stories/menu.stories';
import {
Default as OrganizationProfileDefault,
Expand Down Expand Up @@ -121,6 +128,14 @@ const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes,

const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };

const itemModule: StoryModule = {
meta: itemMeta,
Default: ItemDefault,
Variants: ItemVariants,
Interactive: ItemInteractive,
Group: ItemGroup,
};

const headingModule: StoryModule = {
meta: headingMeta,
Default: HeadingDefault,
Expand Down Expand Up @@ -174,6 +189,7 @@ export const registry: StoryModule[] = [
buttonModule,
cardComponentModule,
inputModule,
itemModule,
dialogComponentModule,
headingModule,
iconModule,
Expand Down
92 changes: 92 additions & 0 deletions packages/swingset/src/stories/item.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as ItemStories from './item.stories';

# Item

Item is a flexible row for lists of accounts, organizations, and settings in Mosaic. It's composed from parts via dot syntax (`Item.Media`, `Item.Content`, `Item.Title`, …) and renders as a `<div>` by default. Pass a `render` prop to make a row an interactive link or button, which adds hover and cursor affordances.

## Example

<Story
name='Default'
storyModule={ItemStories}
/>

### Variants

<Story
name='Variants'
storyModule={ItemStories}
/>

### Interactive

<Story
name='Interactive'
storyModule={ItemStories}
/>

### Group

<Story
name='Group'
storyModule={ItemStories}
/>

## Usage

```tsx
import { Item } from '@clerk/ui/mosaic/components/item';

<Item.Group>
<Item render={({ children, ...props }) => <a {...props} href='/org'>{children}</a>}>
<Item.Media variant='icon'>
<BuildingIcon />
</Item.Media>
<Item.Content>
<Item.Title>Test Organization</Item.Title>
<Item.Description>Member</Item.Description>
</Item.Content>
<Item.Actions>
<Button variant='outline'>Manage</Button>
</Item.Actions>
</Item>
</Item.Group>;
Comment on lines +37 to +53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the usage example self-contained.

Button and BuildingIcon are used but never imported or defined, so this snippet does not compile when copied. Add the public Button import and an icon import/definition, or remove those usages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swingset/src/stories/item.mdx` around lines 37 - 53, Add the
required public Button import and provide a BuildingIcon import or local
definition in the Item usage example so the snippet is self-contained and
compiles when copied; preserve the existing Item.Group structure and usage.

Source: Coding guidelines

```

## Parts

| Part | Slot (`data-cl-slot` / class) | Description |
| ------------------ | ----------------------------- | -------------------------------------------------------------------- |
| `Item` | `cl-item` | Root row. Renders a `<div>`, or a custom element via `render`. |
| `Item.Media` | `cl-item-media` | Leading (or trailing) media: icon, image, or avatar. |
| `Item.Content` | `cl-item-content` | Vertical stack that grows to fill the row between media and actions. |
| `Item.Title` | `cl-item-title` | Primary label. |
| `Item.Description` | `cl-item-description` | Secondary text, clamped to two lines. Renders a `<p>`. |
| `Item.Actions` | `cl-item-actions` | Trailing controls (buttons, badges). |
| `Item.Footer` | `cl-item-footer` | Full-width band below the content row for supplementary metadata. |
| `Item.Group` | `cl-item-group` | Vertical list wrapper (`role="list"`). |
| `Item.Separator` | `cl-item-separator` | Thin divider (`<hr>`) between rows. |

Every part accepts a `render` prop for element polymorphism and forwards a ref.

## Styling

The root reflects its props as `data-*` attributes on `.cl-item`, so consumers can scope overrides without touching StyleX's hashed atoms:

| Prop | Attribute | Values | Default |
| --------- | ------------------ | ----------------------------------- | --------- |
| `variant` | `data-variant` | `default` \| `outline` \| `muted` | `default` |
| `size` | `data-size` | `default` \| `sm` | `default` |
| `render` | `data-interactive` | present when a `render` is provided | — |

```css
/* Tighten a specific list's rows and re-theme the muted variant */
.cl-item[data-size='sm'] {
--cl-spacing: 0.2rem;
}
.cl-item[data-variant='muted'] {
background-color: var(--cl-color-card);
}
```

`Item.Media` also carries `data-variant` (`default` | `icon` | `image`): `icon` renders a bordered `--cl-color-muted` tile, `image` a rounded avatar frame that clips its child. Colors, radii, and spacing all resolve from the Mosaic tokens (`--cl-color-*`, `--cl-radius-*`, `--cl-spacing`).
153 changes: 153 additions & 0 deletions packages/swingset/src/stories/item.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/** @jsxImportSource @emotion/react */
import { Button } from '@clerk/ui/mosaic/components/button';
import { Item } from '@clerk/ui/mosaic/components/item';

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

// Exposes this file's own source (via the `?raw` webpack rule) so each `<Story>` example
// renders a code footer with its function's source. See `StoryModule.__source`.
export { default as __source } from './item.stories?raw';

export const meta: StoryMeta = {
group: 'Components',
title: 'Item',
source: 'packages/ui/src/mosaic/components/item/item.tsx',
};

function BuildingIcon() {
return (
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<path d='M3 21h18M6 21V4a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v17M9 7h.01M9 11h.01M9 15h.01' />
</svg>
);
}

function ArrowIcon() {
return (
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<path d='M5 12h14M13 5l7 7-7 7' />
</svg>
);
}

export function Default() {
return (
<Item>
<Item.Media variant='icon'>
<BuildingIcon />
</Item.Media>
<Item.Content>
<Item.Title>Test Organization</Item.Title>
<Item.Description>Member</Item.Description>
</Item.Content>
<Item.Actions>
<Button variant='outline'>Manage</Button>
</Item.Actions>
</Item>
);
}

export function Variants() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Item variant='default'>
<Item.Content>
<Item.Title>Default</Item.Title>
<Item.Description>Transparent background, no border</Item.Description>
</Item.Content>
</Item>
<Item variant='outline'>
<Item.Content>
<Item.Title>Outline</Item.Title>
<Item.Description>Bordered container</Item.Description>
</Item.Content>
</Item>
<Item variant='muted'>
<Item.Content>
<Item.Title>Muted</Item.Title>
<Item.Description>Subtle filled background</Item.Description>
</Item.Content>
</Item>
</div>
);
}

export function Interactive() {
return (
<Item
render={({ children, ...props }) => (
<a
{...props}
href='#settings'
>
{children}
</a>
)}
>
<Item.Media variant='icon'>
<BuildingIcon />
</Item.Media>
<Item.Content>
<Item.Title>Test Organization</Item.Title>
<Item.Description>Member</Item.Description>
</Item.Content>
<Item.Media>
<ArrowIcon />
</Item.Media>
</Item>
);
}

export function Group() {
return (
<Item.Group>
<Item
render={({ children, ...props }) => (
<a
{...props}
href='#one'
>
{children}
</a>
)}
>
<Item.Content>
<Item.Title>Clerk</Item.Title>
</Item.Content>
</Item>
<Item.Separator />
<Item
render={({ children, ...props }) => (
<a
{...props}
href='#two'
>
{children}
</a>
)}
>
<Item.Content>
<Item.Title>Clerk Cloud</Item.Title>
</Item.Content>
</Item>
</Item.Group>
);
}
2 changes: 2 additions & 0 deletions packages/ui/src/mosaic/components/item/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Item } from './item';
export type { ItemMediaProps, ItemProps } from './item';
Loading
Loading