feat(ui): add Mosaic Item component - #9234
Conversation
🦋 Changeset detectedLatest commit: 497a792 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds a composable Mosaic Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
packages/ui/src/mosaic/components/item/index.ts (1)
1-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAvoid introducing the Item re-export barrel.
The new directory
index.tsadds an unnecessary re-export boundary and the public entry consumes it. Export directly fromitem.tsxinstead to avoid a future circular-dependency edge.
packages/ui/src/mosaic/components/item/index.ts#L1-L2: remove the local re-export barrel.packages/ui/src/mosaic/styles/index.ts#L10-L11: re-export from../components/item/itemdirectly.As per coding guidelines, “Avoid barrel files (
index.tsre-exports) as they can cause circular dependencies.”🤖 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/index.ts` around lines 1 - 2, Remove the Item re-export barrel from packages/ui/src/mosaic/components/item/index.ts lines 1-2. Update packages/ui/src/mosaic/styles/index.ts lines 10-11 to re-export Item, ItemMediaProps, and ItemProps directly from ../components/item/item, preserving the existing public exports.Source: Coding guidelines
packages/swingset/src/stories/item.stories.tsx (1)
17-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd explicit return types to the story components.
Annotate
BuildingIcon,ArrowIcon, and the exported stories with the project’s JSX element return type instead of relying on inference.As per coding guidelines, “Always define explicit return types for functions, especially public APIs.”
Also applies to: 51-153
🤖 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.stories.tsx` around lines 17 - 49, Annotate BuildingIcon, ArrowIcon, and every exported story component in this file with the project’s established JSX element return type. Apply the same explicit return-type convention consistently across the story definitions, including the stories referenced in the additional range, without changing their rendering behavior.Source: Coding guidelines
🤖 Prompt for all review comments with 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.
Inline comments:
In @.changeset/mosaic-item.md:
- Around line 1-2: Add a non-empty changeset for the new public `Item` export
from `@clerk/ui`, specifying the appropriate package bump and a concise release
note describing the API addition. Replace the current empty frontmatter in
`.changeset/mosaic-item.md` while preserving valid changeset formatting.
In `@packages/swingset/src/stories/item.mdx`:
- Around line 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.
In `@packages/ui/src/mosaic/components/item/item.tsx`:
- Around line 149-151: Remove the role="list" assignment from Item.Group in
packages/ui/src/mosaic/components/item/item.tsx (lines 149-151), since it cannot
guarantee listitem children; update the corresponding semantic-role assertion in
packages/ui/src/mosaic/components/item/item.test.tsx (lines 91-101), and remove
or revise the role="list" documentation claim in
packages/swingset/src/stories/item.mdx (line 67).
- Around line 16-168: Integrate the Item surface with the customization contract
by defining stable element descriptors for Root and each slot component: Media,
Content, Title, Description, Actions, Footer, Group, and Separator. Pass the
appropriate descriptor through each renderElement call and wrap the exported
primitive components with the repository’s makeCustomizable HOC so appearance
elements overrides can target every Item element.
---
Nitpick comments:
In `@packages/swingset/src/stories/item.stories.tsx`:
- Around line 17-49: Annotate BuildingIcon, ArrowIcon, and every exported story
component in this file with the project’s established JSX element return type.
Apply the same explicit return-type convention consistently across the story
definitions, including the stories referenced in the additional range, without
changing their rendering behavior.
In `@packages/ui/src/mosaic/components/item/index.ts`:
- Around line 1-2: Remove the Item re-export barrel from
packages/ui/src/mosaic/components/item/index.ts lines 1-2. Update
packages/ui/src/mosaic/styles/index.ts lines 10-11 to re-export Item,
ItemMediaProps, and ItemProps directly from ../components/item/item, preserving
the existing public exports.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 41e7b2f0-ab0b-49bc-bdbf-bed0f2e18d58
📒 Files selected for processing (10)
.changeset/mosaic-item.mdpackages/swingset/src/components/DocsViewer.tsxpackages/swingset/src/lib/registry.tspackages/swingset/src/stories/item.mdxpackages/swingset/src/stories/item.stories.tsxpackages/ui/src/mosaic/components/item/index.tspackages/ui/src/mosaic/components/item/item.styles.tspackages/ui/src/mosaic/components/item/item.test.tsxpackages/ui/src/mosaic/components/item/item.tsxpackages/ui/src/mosaic/styles/index.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
clerk/clerk_go(manual)clerk/dashboard(manual)clerk/accounts(manual)clerk/backoffice(manual)clerk/clerk(manual)clerk/clerk-docs(manual)clerk/cloudflare-workers(manual)clerk/clerk-ios(auto-detected)clerk/cli(auto-detected)clerk/clerk-android(auto-detected)
| --- | ||
| --- |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Add a release entry for the new public UI API.
This PR exports Item from @clerk/ui; an empty changeset risks shipping no version containing the component. Add the appropriate @clerk/ui bump and release note.
Based on learnings, empty changesets are acceptable for documentation-only or internal-only work, not a published public API addition.
🤖 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 @.changeset/mosaic-item.md around lines 1 - 2, Add a non-empty changeset for
the new public `Item` export from `@clerk/ui`, specifying the appropriate
package bump and a concise release note describing the API addition. Replace the
current empty frontmatter in `.changeset/mosaic-item.md` while preserving valid
changeset formatting.
Sources: Coding guidelines, Learnings
| ```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>; |
There was a problem hiding this comment.
🎯 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
| const Root = React.forwardRef<HTMLDivElement, ItemProps>(function MosaicItem( | ||
| { variant = 'default', size = 'default', render, className, style, ...rest }, | ||
| ref, | ||
| ) { | ||
| // A custom render (link/button row) opts into hover + cursor affordances. | ||
| const interactive = Boolean(render); | ||
| return renderElement({ | ||
| defaultTagName: 'div', | ||
| render, | ||
| props: { | ||
| ref, | ||
| ...mergeProps( | ||
| themeProps('item', { variant, size, interactive }), | ||
| stylex.props( | ||
| slots.item.base, | ||
| variant === 'outline' && slots.item.variantOutline, | ||
| variant === 'muted' && slots.item.variantMuted, | ||
| variant === 'default' && slots.item.variantDefault, | ||
| size === 'sm' ? slots.item.sizeSm : slots.item.sizeMd, | ||
| interactive && slots.item.interactive, | ||
| ), | ||
| className, | ||
| style, | ||
| ), | ||
| ...rest, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| export type ItemMediaProps = ComponentProps<'div'> & { | ||
| variant?: 'default' | 'icon' | 'image'; | ||
| }; | ||
|
|
||
| const Media = React.forwardRef<HTMLDivElement, ItemMediaProps>(function MosaicItemMedia( | ||
| { variant = 'default', render, className, style, ...rest }, | ||
| ref, | ||
| ) { | ||
| return renderElement({ | ||
| defaultTagName: 'div', | ||
| render, | ||
| props: { | ||
| ref, | ||
| ...mergeProps( | ||
| themeProps('item-media', { variant }), | ||
| stylex.props( | ||
| slots.media.base, | ||
| variant === 'icon' && slots.media.icon, | ||
| variant === 'image' && slots.media.image, | ||
| ), | ||
| className, | ||
| style, | ||
| ), | ||
| ...rest, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| const Content = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(function MosaicItemContent( | ||
| { render, className, style, ...rest }, | ||
| ref, | ||
| ) { | ||
| return renderElement({ | ||
| defaultTagName: 'div', | ||
| render, | ||
| props: { | ||
| ref, | ||
| ...mergeProps(themeProps('item-content'), stylex.props(slots.content.base), className, style), | ||
| ...rest, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| const Title = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(function MosaicItemTitle( | ||
| { render, className, style, ...rest }, | ||
| ref, | ||
| ) { | ||
| return renderElement({ | ||
| defaultTagName: 'div', | ||
| render, | ||
| props: { ref, ...mergeProps(themeProps('item-title'), stylex.props(slots.title.base), className, style), ...rest }, | ||
| }); | ||
| }); | ||
|
|
||
| const Description = React.forwardRef<HTMLParagraphElement, ComponentProps<'p'>>(function MosaicItemDescription( | ||
| { render, className, style, ...rest }, | ||
| ref, | ||
| ) { | ||
| return renderElement({ | ||
| defaultTagName: 'p', | ||
| render, | ||
| props: { | ||
| ref, | ||
| ...mergeProps(themeProps('item-description'), stylex.props(slots.description.base), className, style), | ||
| ...rest, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| const Actions = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(function MosaicItemActions( | ||
| { render, className, style, ...rest }, | ||
| ref, | ||
| ) { | ||
| return renderElement({ | ||
| defaultTagName: 'div', | ||
| render, | ||
| props: { | ||
| ref, | ||
| ...mergeProps(themeProps('item-actions'), stylex.props(slots.actions.base), className, style), | ||
| ...rest, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| const Footer = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(function MosaicItemFooter( | ||
| { render, className, style, ...rest }, | ||
| ref, | ||
| ) { | ||
| return renderElement({ | ||
| defaultTagName: 'div', | ||
| render, | ||
| props: { ref, ...mergeProps(themeProps('item-footer'), stylex.props(slots.band.base), className, style), ...rest }, | ||
| }); | ||
| }); | ||
|
|
||
| const Group = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(function MosaicItemGroup( | ||
| { render, className, style, ...rest }, | ||
| ref, | ||
| ) { | ||
| return renderElement({ | ||
| defaultTagName: 'div', | ||
| render, | ||
| props: { | ||
| ref, | ||
| role: 'list', | ||
| ...mergeProps(themeProps('item-group'), stylex.props(slots.group.base), className, style), | ||
| ...rest, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| const Separator = React.forwardRef<HTMLHRElement, ComponentProps<'hr'>>(function MosaicItemSeparator( | ||
| { render, className, style, ...rest }, | ||
| ref, | ||
| ) { | ||
| return renderElement({ | ||
| defaultTagName: 'hr', | ||
| render, | ||
| props: { | ||
| ref, | ||
| ...mergeProps(themeProps('item-separator'), stylex.props(slots.separator.base), className, style), | ||
| ...rest, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Wire Item slots into the UI customization contract.
The new themable Item surface only emits themeProps/StyleX classes; no slot receives an elementDescriptor or is wrapped through the customizable primitive path. Appearance elements overrides therefore cannot target this public component consistently.
Add stable descriptors for the root and slots, then apply the repository’s elementDescriptor/makeCustomizable integration.
As per coding guidelines, “Use element descriptors for all themable elements” and “Wrap primitive components with makeCustomizable HOC.”
🤖 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.tsx` around lines 16 - 168,
Integrate the Item surface with the customization contract by defining stable
element descriptors for Root and each slot component: Media, Content, Title,
Description, Actions, Footer, Group, and Separator. Pass the appropriate
descriptor through each renderElement call and wrap the exported primitive
components with the repository’s makeCustomizable HOC so appearance elements
overrides can target every Item element.
Source: Coding guidelines
| role: 'list', | ||
| ...mergeProps(themeProps('item-group'), stylex.props(slots.group.base), className, style), | ||
| ...rest, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not expose a list role without list-item children.
Item.Group declares an ARIA list, but its direct Item and Item.Separator children are div/hr or arbitrary render targets, not listitems. Either remove the list role or redesign the group API to enforce semantic list items.
packages/ui/src/mosaic/components/item/item.tsx#L149-L151: removerole: 'list'unless the component can guaranteelistitemchildren.packages/ui/src/mosaic/components/item/item.test.tsx#L91-L101: update the test to reflect the corrected semantic contract.packages/swingset/src/stories/item.mdx#L67-L67: remove or revise therole="list"documentation claim.
As per coding guidelines, “Use proper HTML elements for semantic HTML in React components.”
📍 Affects 3 files
packages/ui/src/mosaic/components/item/item.tsx#L149-L151(this comment)packages/ui/src/mosaic/components/item/item.test.tsx#L91-L101packages/swingset/src/stories/item.mdx#L67-L67
🤖 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.tsx` around lines 149 - 151,
Remove the role="list" assignment from Item.Group in
packages/ui/src/mosaic/components/item/item.tsx (lines 149-151), since it cannot
guarantee listitem children; update the corresponding semantic-role assertion in
packages/ui/src/mosaic/components/item/item.test.tsx (lines 91-101), and remove
or revise the role="list" documentation claim in
packages/swingset/src/stories/item.mdx (line 67).
Source: Coding guidelines
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/electron
@clerk/electron-passkeys
@clerk/eslint-plugin
@clerk/expo
@clerk/expo-google-signin
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/hono
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
API Changes Report
Summary
🔴 Breaking changes index (84)Every breaking change, up front. Full diffs are in the package sections below.
@clerk/backendVersion: 3.13.2 → 3.13.0 🔴 Breaking Changes (81)Changed:
|
The Mosaic StyleX build resolved @clerk/headless types against its published dist .d.ts re-export barrels, which rolldown-plugin-dts can't follow when inlining a bundled dependency. Since @clerk/headless is private and must be inlined, map it to source in tsconfig.mosaic.json (the monorepo default, same pattern already used for @/core/*), so Mosaic components can import headless types directly.
Add a Mosaic `Item` component for lists of accounts, organizations, and settings rows. Composed via dot syntax (`Item.Media`, `Item.Content`, `Item.Title`, `Item.Description`, `Item.Actions`, `Item.Footer`, `Item.Group`, `Item.Separator`) with `default`/`outline`/`muted` variants and `default`/`sm` sizes. Per-slot StyleX styles compile into the shared sheet, and a `render` prop allows an item to render as a link or button. Includes swingset docs and stories.
fbbe316 to
1ac76ed
Compare
569563b to
1d86c3e
Compare
- 8px padding on the row; media slot fits its child (no fixed-width variants) - drop Item variant/size props and the Footer part - examples use an avatar in the media slot; interactive trailing slot is 28px
1d86c3e to
45d7685
Compare
The public contract is now the `.cl-<slot>` class plus `data-<axis>` attrs emitted by `themeProps`/`mergeStyleProps`, not `data-cl-slot`. Mark the slot-recipe engine and its `data-cl-*`/`appearance.elements` cascade as the legacy authoring path still used by un-migrated components.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with 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.
Inline comments:
In `@packages/ui/src/mosaic/components/item/item.styles.ts`:
- Around line 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.
In `@packages/ui/src/mosaic/components/item/item.tsx`:
- Around line 8-11: The Item public API is incomplete: add typed variant and
size props, map them to the component’s styling axes, implement the Item.Footer
slot, and assign it on the exported Item component. Update the associated tests
and documentation to cover these APIs, using the existing Item styling and slot
patterns.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: b43b47ad-829e-418f-807a-8b03e013113d
📒 Files selected for processing (11)
.changeset/mosaic-item.mdpackages/swingset/src/components/DocsViewer.tsxpackages/swingset/src/lib/registry.tspackages/swingset/src/stories/item.mdxpackages/swingset/src/stories/item.stories.tsxpackages/ui/src/mosaic/components/item/index.tspackages/ui/src/mosaic/components/item/item.styles.tspackages/ui/src/mosaic/components/item/item.test.tsxpackages/ui/src/mosaic/components/item/item.tsxpackages/ui/src/mosaic/styles/index.tspackages/ui/tsconfig.mosaic.json
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
clerk/clerk_go(manual)clerk/dashboard(manual)clerk/accounts(manual)clerk/backoffice(manual)clerk/clerk(manual)clerk/clerk-docs(manual)clerk/cloudflare-workers(manual)
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/ui/src/mosaic/components/item/index.ts
- packages/ui/src/mosaic/styles/index.ts
- .changeset/mosaic-item.md
- packages/swingset/src/components/DocsViewer.tsx
| 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'], | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -C3 'WebkitLineClamp|WebkitBoxOrient|display:\s*.*-webkit-box' packages/ui/srcRepository: 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:
- 1: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/display
- 2: https://developer.mozilla.org/en-US/docs/Web/CSS/display?summary=
- 3: https://github.com/mdn/content/blob/main/files/en-us/web/css/reference/properties/display/index.md
- 4: https://drafts.csswg.org/css-display/
- 5: https://github.com/mdn/content/blob/main/files/en-us/web/css/guides/display/multi-keyword_syntax/index.md
- 6: https://www.w3.org/TR/css-display-3/
🌐 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 40Repository: 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.tsRepository: 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:
- 1: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/line-clamp
- 2: CSS
line-clampproperty web-platform-tests/interop#1029 - 3: https://drafts.csswg.org/css-overflow-4/
- 4: https://bugzilla.mozilla.org/show_bug.cgi?id=1540681
- 5: https://www.geeksforgeeks.org/css/css-webkit-line-clamp-property/
- 6: https://stackoverflow.com/questions/41995229/webkit-line-clamp-does-not-work-on-chrome-unless-i-make-some-unrelated-css-chan
- 7: https://support.arraynetworks.net/prx/000/https/drafts.csswg.org/css-overflow-4/
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.
| 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 type ItemProps = Omit<ComponentProps<'div'>, 'render'> & { | ||
| /** Render a custom element (e.g. a link or button) in place of the default `div`. */ | ||
| render?: RenderProp<React.HTMLAttributes<HTMLElement>>; | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Implement the advertised Item public API.
variant and size are neither declared nor mapped to styling axes, and Item.Footer is not implemented or assigned. Consumers following this PR’s contract will get TypeScript errors or undefined at runtime. Add the props, styles, footer slot, and corresponding tests/docs.
Also applies to: 147-155
🤖 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.tsx` around lines 8 - 11, The
Item public API is incomplete: add typed variant and size props, map them to the
component’s styling axes, implement the Item.Footer slot, and assign it on the
exported Item component. Update the associated tests and documentation to cover
these APIs, using the existing Item styling and slot patterns.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
references/mosaic-architecture.md (1)
124-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a migration guide for the styling-contract transition.
The documentation introduces new
.cl-*/data-*selectors alongside the legacydata-cl-*contract, but provides no before/after migration example. Add a short migration section explaining which selectors apply to legacy and StyleX-authored components.As per coding guidelines, Markdown files must include migration guides for breaking changes.
🤖 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 `@references/mosaic-architecture.md` around lines 124 - 126, Add a concise migration section near “Appearance & cascade” documenting the transition from legacy data-cl-* selectors to the new .cl-* and data-* contract. Include a before/after example and clearly state which selectors apply to Emotion slot-recipe components versus StyleX-authored components, including that StyleX uses className/style and does not read appearance.elements.Source: Coding guidelines
🤖 Prompt for all review comments with 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.
Inline comments:
In `@references/mosaic-architecture.md`:
- Line 9: Update the styling contract statements in the overview and API
sections to scope `.cl-<slot>` classes and `data-<axis>` attributes to
StyleX-authored parts only. Explicitly distinguish legacy slot-recipe
components, which retain the `data-cl-slot` and `data-cl-*` contract, and remove
any wording that implies every Mosaic part emits the StyleX class contract.
---
Nitpick comments:
In `@references/mosaic-architecture.md`:
- Around line 124-126: Add a concise migration section near “Appearance &
cascade” documenting the transition from legacy data-cl-* selectors to the new
.cl-* and data-* contract. Include a before/after example and clearly state
which selectors apply to Emotion slot-recipe components versus StyleX-authored
components, including that StyleX uses className/style and does not read
appearance.elements.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: bb2d4296-cad5-49f0-b9e0-a57c3da0ef98
📒 Files selected for processing (1)
references/mosaic-architecture.md
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
clerk/clerk_go(manual)clerk/dashboard(manual)clerk/accounts(manual)clerk/backoffice(manual)clerk/clerk(manual)clerk/clerk-docs(manual)clerk/cloudflare-workers(manual)
| Mosaic uses Emotion for CSS-in-JS but delivers theme tokens via its own React context (not Emotion's `ThemeProvider`). This avoids type conflicts with the existing system's `InternalTheme` augmentation on Emotion's global `Theme` interface. | ||
|
|
||
| The public styling contract is **data attributes** (`data-cl-slot`, `data-cl-<state>`, `data-cl-<variant>`), the same convention shipping in `@clerk/headless`. There is no classname derivation, no `__state` concatenation, and no central appearance-key registry. Components are authored with **slot recipes** (`defineSlotRecipe`), which own variants, slot identity, state→attribute mapping, and the appearance cascade in one place. | ||
| The public styling contract is a **stable per-slot class** plus **`data-<axis>` attributes**: a part emits `class="cl-<slot>"` for its identity (`.cl-button`, `.cl-item`) and `data-<axis>="<value>"` / presence `data-<state>` for its variants and state. Consumers target those, never StyleX's hashed atoms; tokens ship as overridable `--cl-*` custom properties. Newer components are authored with **StyleX** (`stylex.create` + the `themeProps`/`mergeStyleProps` helpers in `props.ts`). The slot-recipe engine described further below (`defineSlotRecipe`, `useRecipe`, and its older `data-cl-slot`/`data-cl-*` attribute contract) is the legacy authoring path still used by un-migrated components during the migration. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Scope the new styling contract to StyleX-authored parts.
Lines 9 and 163 state that legacy slot-recipe components still use data-cl-slot, but line 89 says every Mosaic part emits .cl-<slot>. This can lead consumers to target selectors that legacy components do not render. Qualify the overview and API section to distinguish StyleX components from the legacy contract.
Also applies to: 89-95, 163-164
🤖 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 `@references/mosaic-architecture.md` at line 9, Update the styling contract
statements in the overview and API sections to scope `.cl-<slot>` classes and
`data-<axis>` attributes to StyleX-authored parts only. Explicitly distinguish
legacy slot-recipe components, which retain the `data-cl-slot` and `data-cl-*`
contract, and remove any wording that implies every Mosaic part emits the StyleX
class contract.
Summary
Adds a new Item component to Mosaic, styled with StyleX following the Button migration (
themeProps+stylex.props+mergeProps, public contract of.cl-*class +data-*axes). The API matches shadcn's Item, composed via dot syntax.Item is a row for lists of accounts, organizations, and settings (org switcher, account switcher, settings rows).
Parts
Item(root) —variant(default|outline|muted),size(default|sm)Item.Media—variant(default|icon|image)Item.Content,Item.Title,Item.Description,Item.Actions,Item.FooterItem.Group(role="list"),Item.SeparatorNotes
renderprop (viarenderElement), not shadcn'sasChild. The root'srenderis typed generically (likeBox) so a row can render as a link or button.renderbeing present (reflected asdata-interactive), replacing shadcn's[a]:hoverselector: a plain<Item>div is static; a rendered link/button row gets cursor + muted hover.stylex.createblocks.Item.Header(dropped per review);Item.Footerretained.accenttoken, so hover/muted use--cl-color-mutedviacolor-mix.Docs
Swingset docs added (
item.stories.tsx+item.mdx, Archetype A·compound) and wired intoregistry.ts/DocsViewer.tsx.Testing
pnpm vitest run(ui) — 8/8 passtsc --noEmitclean (ui + swingset)@stylexjsrules)pnpm formatclean