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
14 changes: 14 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ import {
meta as inputMeta,
Sizes as InputSizes,
} from '../stories/input.stories';
import {
Default as ItemDefault,
Group as ItemGroup,
Interactive as ItemInteractive,
meta as itemMeta,
} from '../stories/item.stories';
import { meta as menuMeta } from '../stories/menu.stories';
import {
Default as OrganizationProfileDefault,
Expand Down Expand Up @@ -121,6 +127,13 @@ const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes,

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

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

const headingModule: StoryModule = {
meta: headingMeta,
Default: HeadingDefault,
Expand Down Expand Up @@ -174,6 +187,7 @@ export const registry: StoryModule[] = [
buttonModule,
cardComponentModule,
inputModule,
itemModule,
dialogComponentModule,
headingModule,
iconModule,
Expand Down
79 changes: 79 additions & 0 deletions packages/swingset/src/stories/item.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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}
/>

### 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>
<img src={org.imageUrl} alt='' width={36} height={36} />
</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 +30 to +46

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 | 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.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 state as `data-*` attributes on `.cl-item`, so consumers can scope overrides without touching StyleX's hashed atoms:

| Prop | Attribute | Values | Default |
| -------- | ------------------ | ----------------------------------- | ------- |
| `render` | `data-interactive` | present when a `render` is provided | — |

```css
/* Re-theme interactive rows */
.cl-item[data-interactive] {
background-color: var(--cl-color-card);
}
```

`Item.Media` sizes to its child and centers it; its height follows the row. Bring your own icon, avatar, or image at whatever dimensions you need (the examples use a 36px avatar and a 28px trailing icon), then size the slot by sizing that child. Colors, radii, and spacing all resolve from the Mosaic tokens (`--cl-color-*`, `--cl-radius-*`, `--cl-spacing`).
140 changes: 140 additions & 0 deletions packages/swingset/src/stories/item.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/** @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',
};

// A square org/account avatar sized for the media slot (36px). Since `Item.Media`
// fits its child, the avatar's own dimensions drive the slot width.
function Avatar({ initial }: { initial: string }) {
return (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: '2.25rem',
height: '2.25rem',
borderRadius: 'var(--cl-radius-inner)',
backgroundColor: 'var(--cl-color-primary)',
color: 'var(--cl-color-primary-foreground)',
fontSize: '0.875rem',
fontWeight: 600,
}}
>
{initial}
</span>
);
}

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>
<Avatar initial='T' />
</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 Interactive() {
return (
<Item
render={({ children, ...props }) => (
<a
{...props}
href='#settings'
>
{children}
</a>
)}
>
<Item.Media>
<Avatar initial='T' />
</Item.Media>
<Item.Content>
<Item.Title>Test Organization</Item.Title>
<Item.Description>Member</Item.Description>
</Item.Content>
<Item.Media style={{ width: '1.75rem' }}>
<ArrowIcon />
</Item.Media>
</Item>
);
}

export function Group() {
return (
<Item.Group>
<Item
render={({ children, ...props }) => (
<a
{...props}
href='#one'
>
{children}
</a>
)}
>
<Item.Media>
<Avatar initial='C' />
</Item.Media>
<Item.Content>
<Item.Title>Clerk</Item.Title>
</Item.Content>
</Item>
<Item.Separator />
<Item
render={({ children, ...props }) => (
<a
{...props}
href='#two'
>
{children}
</a>
)}
>
<Item.Media>
<Avatar initial='C' />
</Item.Media>
<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 { ItemProps } from './item';
116 changes: 116 additions & 0 deletions packages/ui/src/mosaic/components/item/item.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as stylex from '@stylexjs/stylex';

import { colorVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex';

export const item = stylex.create({
base: {
padding: space['2'],
borderRadius: radiusVars['--cl-radius-element'],
gap: space['3'],
outline: {
default: 'none',
':focus-visible': `2px solid color-mix(in oklab, ${colorVars['--cl-color-primary']} 50%, transparent)`,
},
alignItems: 'center',
boxSizing: 'border-box',
color: colorVars['--cl-color-card-foreground'],
display: 'flex',
flexWrap: 'wrap',
fontFamily: 'inherit',
fontSize: typeScaleVars['--cl-text-label-size'],
lineHeight: typeScaleVars['--cl-text-label-leading'],
outlineOffset: '2px',
textAlign: 'start',
transitionDuration: '150ms',
transitionProperty: 'background-color, color',
width: '100%',
},

// interactive rows (rendered as a link/button via `render`) gain hover + cursor
interactive: {
backgroundColor: {
default: null,
':active': `color-mix(in oklab, ${colorVars['--cl-color-muted']} 70%, transparent)`,
'@media (hover: hover)': {
':hover': `color-mix(in oklab, ${colorVars['--cl-color-muted']} 50%, transparent)`,
},
},
cursor: 'pointer',
},
});

export const media = stylex.create({
// sizes to its child; height follows the row (taller on a 2-line item, shorter on a button).
base: {
gap: space['2'],
alignItems: 'center',
display: 'flex',
flexShrink: 0,
justifyContent: 'center',
width: 'fit-content',
},
});

export const content = stylex.create({
base: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
justifyContent: 'center',
minWidth: 0,
},
});

export const title = stylex.create({
base: {
gap: space['2'],
alignItems: 'center',
color: colorVars['--cl-color-card-foreground'],
display: 'flex',
fontSize: typeScaleVars['--cl-text-label-size'],
fontWeight: typeScaleVars['--cl-text-label-weight'],
lineHeight: typeScaleVars['--cl-text-label-leading'],
width: 'fit-content',
},
});

export const description = stylex.create({
base: {
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 2,
color: colorVars['--cl-color-muted-foreground'],
fontSize: typeScaleVars['--cl-text-label-sm-size'],
fontWeight: 400,
lineHeight: typeScaleVars['--cl-text-label-sm-leading'],
},
});
Comment on lines +77 to +87

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'WebkitLineClamp|WebkitBoxOrient|display:\s*.*-webkit-box' packages/ui/src

Repository: clerk/javascript

Length of output: 874


🌐 Web query:

MDN -webkit-line-clamp requires -webkit-box display

💡 Result:

The MDN Web Docs documentation on the display CSS property explains that it defines an element's inner and outer display types, which determine how the element participates in layout (outer type) and how its children are laid out (inner type) [1][2][3]. There is no "requirement" for display in the sense of a mandatory setting; rather, the property provides the mechanism to control layout [1][4]. Key technical details include: 1. Two-Value Syntax: The display property can accept one or two keywords [1][5]. The multi-keyword syntax allows developers to explicitly define both the outer and inner display types (e.g., block flow, block flex) [1][5]. 2. Backward Compatibility: The traditional single-keyword values (such as block, inline, flex, grid) are supported for backward compatibility and are considered "legacy" values [1][5]. These single values automatically map to a combined outer/inner value [1][2]. For example, using only an inner keyword like flex or grid causes the outer display type to default to block [1][5]. 3. Layout Modes: The property determines how an element generates boxes and interacts with flow layout, grid, or flexbox, among other formatting contexts [1][3][6]. The CSS Display Module Level 3 specification defines these behaviors and is the technical standard that the MDN documentation reflects [6][4][5].

Citations:


🌐 Web query:

MDN -webkit-line-clamp property display webkit-box required

💡 Result:

The display: -webkit-box property is technically required if you are using the legacy -webkit-line-clamp property to truncate text [1]. While modern CSS is introducing a standardized line-clamp property, the legacy -webkit-line-clamp approach relies on specific proprietary behaviors associated with the older -webkit-box layout model [1]. For this legacy clamping to function, the following properties must be present [1]: 1. display: -webkit-box or display: -webkit-inline-box [1] 2. -webkit-box-orient: vertical [1] 3. -webkit-line-clamp: [1] If these conditions are not met—specifically if the display value is set to a modern value like flex instead of the legacy -webkit-box—the legacy line-clamp mechanism will not engage, as the element will be treated as a standard flex container rather than the legacy block container required for that specific implementation [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== item.styles.ts relevant section =="
sed -n '60,95p' packages/ui/src/mosaic/components/item/item.styles.ts

echo
echo "== item component usages of description =="
rg -n -C4 'item\.description|description\(' packages/ui/src/mosaic/components packages/ui/src | head -n 120

echo
echo "== stylex package/config =="
fd -a 'stylex' package.json tsconfig*.json . | head -n 40

Repository: clerk/javascript

Length of output: 3203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate mosaic item files =="
fd -a 'item' packages/ui/src/mosaic/components | sed 's#^\./##'

echo
echo "== imports/usages referencing stylex.create or item styles =="
rg -n "from ['\"]\\.\\.\\/\\.\\.\\/item\\.styles['\"]|from ['\"]\\.\\/item\\.styles['\"]|stylex\\.create|WebkitLineClamp|description" packages/ui/src/mosaic packages/ui/src/components packages/ui/src | head -n 200

echo
echo "== package refs for stylex ="
rg -n '"`@stylexjs/`|stylex' package.json packages -g 'package.json' -g '*.ts' -g '*.tsx' | head -n 120

echo
echo "== stylex declaration/token syntax examples =="
sed -n '1,160p' packages/ui/src/mosaic/tokens.stylex.ts

Repository: clerk/javascript

Length of output: 39165


🌐 Web query:

MDN line-clamp CSS property does not require -webkit-box display

💡 Result:

The statement that the CSS line-clamp property does not require display is correct for the standardized version of the property, but incorrect for the commonly used, legacy vendor-prefixed version [1][2]. 1. Standardized line-clamp: The standardized CSS line-clamp property (defined in the CSS Overflow Module Level 4) is designed to function without the specific legacy requirements of its predecessor [3][4]. It is a shorthand property that controls line limiting through internal mechanisms (max-lines, block-ellipsis, and continue) and does not depend on the specific display or box-orient properties required by the legacy version [3][2]. 2. Vendor-prefixed -webkit-line-clamp: The widely used legacy property, -webkit-line-clamp, specifically requires the display property to be set to -webkit-box or -webkit-inline-box, in combination with -webkit-box-orient: vertical [1][5][6]. Because this legacy property is still heavily used for compatibility, this dependency is a well-known and documented requirement [1][7]. In summary, while the modern, standardized line-clamp property does not have these restrictions, the prefixed version that developers often encounter in existing codebases strictly requires them [1][2].

Citations:


Enable the two-line clamp.

WebkitLineClamp only takes effect when the legacy box layout is enabled, so descriptions can overflow their intended two-line height.

Proposed fix
 base: {
+  display: '-webkit-box',
   overflow: 'hidden',
   WebkitBoxOrient: 'vertical',
   WebkitLineClamp: 2,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const description = stylex.create({
base: {
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 2,
color: colorVars['--cl-color-muted-foreground'],
fontSize: typeScaleVars['--cl-text-label-sm-size'],
fontWeight: 400,
lineHeight: typeScaleVars['--cl-text-label-sm-leading'],
},
});
export const description = stylex.create({
base: {
display: '-webkit-box',
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 2,
color: colorVars['--cl-color-muted-foreground'],
fontSize: typeScaleVars['--cl-text-label-sm-size'],
fontWeight: 400,
lineHeight: typeScaleVars['--cl-text-label-sm-leading'],
},
});
🤖 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/ui/src/mosaic/components/item/item.styles.ts` around lines 77 - 87,
Update the description style in the stylex.create definition to enable the
required legacy WebKit box layout alongside WebkitLineClamp, preserving the
existing two-line clamp and other styling.


export const actions = stylex.create({
base: {
gap: space['2'],
alignItems: 'center',
display: 'flex',
flexShrink: 0,
},
});

export const group = stylex.create({
base: {
display: 'flex',
flexDirection: 'column',
width: '100%',
},
});

export const separator = stylex.create({
base: {
borderStyle: 'none',
marginBlock: space['2'],
marginInline: 0,
backgroundColor: colorVars['--cl-color-border'],
flexShrink: 0,
height: '1px',
width: '100%',
},
});
Loading
Loading