Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
97efb64
refactor(ui): migrate Mosaic Input to StyleX
austincalvelage Jul 31, 2026
a317211
test(ui): cover Mosaic Input behavior
austincalvelage Jul 31, 2026
5751227
chore(repo): add Input migration changeset
austincalvelage Jul 31, 2026
11086f3
fix(ui): align Mosaic Input with Figma
austincalvelage Jul 31, 2026
fb4ad8e
fix(ui): use placeholder color token
austincalvelage Jul 31, 2026
6d3249d
fix(ui): theme Input focus ring
austincalvelage Jul 31, 2026
4ec5b34
fix(ui): preserve Input focus when invalid
austincalvelage Jul 31, 2026
496b0e7
fix(ui): align Mosaic Input interaction states
austincalvelage Aug 3, 2026
bc2244c
fix(ui): refine Mosaic Input interaction states
austincalvelage Aug 3, 2026
f75dab1
Update --cl-color-border token
austincalvelage Aug 3, 2026
38e7618
feat(ui): add Mosaic font family token
austincalvelage Aug 3, 2026
4c2cf4b
fix(ui): prevent iOS zoom on Mosaic inputs
austincalvelage Aug 3, 2026
47eca0a
feat(ui): add Mosaic input color token
austincalvelage Aug 3, 2026
c7c16b0
fix(ui): apply Mosaic reset to Input
austincalvelage Aug 5, 2026
d64573d
feat(ui): add Mosaic TextField component
austincalvelage Aug 3, 2026
9e1ccea
docs(swingset): add Mosaic TextField stories
austincalvelage Aug 3, 2026
d393db8
docs(swingset): fix horizontal field example
austincalvelage Aug 3, 2026
066371e
refactor(ui): replace TextField with semantic Field
austincalvelage Aug 4, 2026
e8624b7
docs(swingset): replace TextField stories with Field
austincalvelage Aug 4, 2026
fc28027
fix(ui): align field description icon
austincalvelage Aug 4, 2026
8f248db
docs(swingset): align Field docs with StyleX
austincalvelage Aug 5, 2026
5634851
feat(ui): automatically associate Mosaic Field controls
austincalvelage Aug 5, 2026
d2d368f
docs(swingset): document automatic Field associations
austincalvelage Aug 5, 2026
59c0e38
fix(ui): align Field error icon to line height
austincalvelage Aug 5, 2026
b387d17
fix(ui): align Field label color with Figma
austincalvelage Aug 5, 2026
7c6439d
feat(ui): add Mosaic profile icons
austincalvelage Aug 4, 2026
257acfe
feat(ui): add Mosaic user profile panel
austincalvelage Aug 4, 2026
c76fe52
docs(swingset): add user profile panel examples
austincalvelage Aug 4, 2026
00498e6
refactor(ui): use Field in user profile panel
austincalvelage Aug 5, 2026
ef4c07c
refactor(ui): inherit user profile Field label styles
austincalvelage Aug 5, 2026
a0c68e9
refactor(ui): use Menu for user profile actions
austincalvelage Aug 5, 2026
da78d2b
feat(ui): add user profile item actions
austincalvelage Aug 5, 2026
1c2be63
fix(ui): align user profile text color
austincalvelage Aug 5, 2026
d8c3cdb
fix(ui): align user profile label weight
austincalvelage Aug 5, 2026
0d62373
fix(ui): prevent user profile layout shift
austincalvelage Aug 5, 2026
42cd165
feat(ui): add outlined card elevation
austincalvelage Aug 6, 2026
3781cbf
feat(ui): refine user profile panel layout
austincalvelage Aug 6, 2026
5832e60
feat(ui): add web3 wallets to user profile
austincalvelage Aug 6, 2026
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/migrate-mosaic-input-to-stylex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
5 changes: 5 additions & 0 deletions .changeset/mosaic-user-profile-profile-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/ui': patch
---

Add an outlined Card elevation for bordered, non-elevated surfaces, make Item groups layout-only, and expose Web3 wallets in the Mosaic user profile panel.
2 changes: 2 additions & 0 deletions .changeset/quiet-fields-compose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 4 additions & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { ViewSource } from './ViewSource';
// MDX docs keyed by `group` slug → `component` slug. Group-aware so identically-named
// entries (the headless `Dialog` primitive vs. the styled `Dialog` component) stay distinct.
const docModules: Record<string, Record<string, React.ComponentType>> = {
user: {
'user-profile-profile-panel': dynamic(() => import('../stories/user-profile-profile-panel.mdx')),
},
organization: {
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),
'organization-profile-general-panel': dynamic(() => import('../stories/organization-profile-general-panel.mdx')),
Expand Down Expand Up @@ -41,6 +44,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
popover: dynamic(() => import('../stories/popover.component.mdx')),
tabs: dynamic(() => import('../stories/tabs.component.mdx')),
text: dynamic(() => import('../stories/text.mdx')),
field: dynamic(() => import('../stories/field.component.mdx')),
},
primitives: {
// Headless primitives — alphabetical.
Expand Down
18 changes: 18 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Default as DestructiveDefault, meta as destructiveMeta } from '../stori
import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories';
import { meta as dialogMeta } from '../stories/dialog.stories';
import { meta as drawerMeta } from '../stories/drawer.stories';
import { Default as FieldDefault, meta as fieldMeta } from '../stories/field.component.stories';
import { meta as fileUploadMeta } from '../stories/file-upload.stories';
import {
Colors as HeadingColors,
Expand Down Expand Up @@ -115,6 +116,10 @@ import {
} from '../stories/text.stories';
import { meta as tooltipMeta } from '../stories/tooltip.stories';
import { meta as useDataTableMeta } from '../stories/use-data-table.stories';
import {
Default as UserProfileProfilePanelDefault,
meta as userProfileProfilePanelMeta,
} from '../stories/user-profile-profile-panel.stories';
import { toSlug } from './slug';
import type { StoryModule } from './types';

Expand Down Expand Up @@ -200,6 +205,11 @@ const tabsComponentModule: StoryModule = { meta: tabsComponentMeta, Default: Tab

const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: TextSizes, Colors: TextColors };

const fieldModule: StoryModule = {
meta: fieldMeta,
Default: FieldDefault,
};

const iconModule: StoryModule = {
meta: iconMeta,
Default: IconDefault,
Expand Down Expand Up @@ -236,7 +246,14 @@ const scrollAreaModule: StoryModule = {

const useDataTableModule: StoryModule = { meta: useDataTableMeta };

const userProfileProfilePanelModule: StoryModule = {
meta: userProfileProfilePanelMeta,
Default: UserProfileProfilePanelDefault,
};

export const registry: StoryModule[] = [
// User
userProfileProfilePanelModule,
// Organization
organizationProfileModule,
organizationProfileGeneralPanelModule,
Expand All @@ -262,6 +279,7 @@ export const registry: StoryModule[] = [
popoverComponentModule,
tabsComponentModule,
textModule,
fieldModule,
// Primitives — alphabetical within the group.
accordionModule,
autocompleteModule,
Expand Down
2 changes: 1 addition & 1 deletion packages/swingset/src/stories/card.component.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const meta: StoryMeta = {
styles: {
_variants: {
alignment: { start: {}, center: {} },
elevation: { card: {}, flush: {}, overlay: {} },
elevation: { card: {}, flush: {}, outlined: {}, overlay: {} },
},
_defaultVariants: {
alignment: 'start',
Expand Down
61 changes: 61 additions & 0 deletions packages/swingset/src/stories/field.component.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as FieldStories from './field.component.stories';

# Field

The Mosaic `Field` provides StyleX-themed parts for composing labels, supporting text, and validation errors around a form control. Its context automatically connects Mosaic controls to the rendered label and messages.

## Example

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

## Usage

Compose one Mosaic control inside each `Field.Root` to generate its ID, the label's `htmlFor`, and the message relationships. Rendering multiple controls logs a development warning. The caller owns validation and decides when to render an error. Use a separate `Field.Root` for each control; grouped controls should use native `<fieldset>` and `<legend>` semantics until dedicated Mosaic `Fieldset` and `Field.Item` components are available.

```tsx
import { Field } from '@clerk/ui/mosaic/components/field';
import { Input } from '@clerk/ui/mosaic/components/input';

<Field.Root>
<Field.Label>Email address</Field.Label>
<Input
name='email'
type='email'
required
aria-invalid={Boolean(error)}
/>
{error ? <Field.Error>{error}</Field.Error> : <Field.Description>Used for account notifications.</Field.Description>}
</Field.Root>;
```

Explicit `id`, `htmlFor`, `aria-labelledby`, and `aria-describedby` values remain supported. Field preserves explicit IDs after hydration and merges external ARIA references with its generated relationships. During server rendering, Field emits its generated control ID and native label relationship; explicit control IDs and generated label and message ARIA references finalize during hydration. `name` still identifies the submitted form value and is typically what form libraries use for registration.

Field does not validate controls, propagate semantic state, or render errors automatically. Its parts may also be used independently without `Field.Root`.

## Parts

| Part | Stable slot class | Description |
| ------------------- | ----------------------- | ------------------------------------------------- |
| `Field.Root` | `.cl-field-root` | Unstyled `div` and field context provider. |
| `Field.Label` | `.cl-field-label` | Native `label` associated with the field control. |
| `Field.Description` | `.cl-field-description` | Supporting `p` associated with the field control. |
| `Field.Error` | `.cl-field-error` | Associated error `p` with an alert icon. |

## Styling

The Mosaic field is themed with **StyleX**. Each styled part carries the stable public slot class shown above alongside the generated StyleX atoms. Consumers never target the hashed atomic classes—override a `.cl-field-*` class from a CSS layer that wins over `@clerk/ui/styles.css`:

```css
@import '@clerk/ui/styles.css' layer(components);

@layer overrides {
.cl-field-label {
font-weight: 600;
}
}
```

`Field.Root` ships no layout. Higher-level blocks own how its parts are arranged; for example, a settings row can provide the grid and alignment for a field. Customize an `Input` through its exposed tokens, `.cl-input`, `className`, and `style`; Field does not add control-specific styling.
35 changes: 35 additions & 0 deletions packages/swingset/src/stories/field.component.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Field } from '@clerk/ui/mosaic/components/field';
import { Input } from '@clerk/ui/mosaic/components/input';

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 './field.component.stories?raw';

export const meta: StoryMeta = {
group: 'Components',
title: 'Field',
source: 'packages/ui/src/mosaic/components/field/field.tsx',
styleEngine: 'stylex',
};

const stackStyles = {
display: 'grid',
gap: 8,
maxWidth: 384,
} as const;

export function Default() {
return (
<Field.Root style={stackStyles}>
<Field.Label>Email address</Field.Label>
<Input
name='email'
type='email'
placeholder='you@example.com'
/>
<Field.Description>Used for account notifications.</Field.Description>
</Field.Root>
);
}
7 changes: 6 additions & 1 deletion packages/swingset/src/stories/icon.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const meta: StoryMeta = {
styleEngine: 'stylex',
styles: {
_variants: {
size: { sm: {}, md: {}, lg: {} },
size: { xs: {}, sm: {}, md: {}, lg: {} },
},
_defaultVariants: {
size: 'md',
Expand All @@ -44,6 +44,11 @@ export function Default(props: Record<string, unknown>) {
export function Sizes(props: Record<string, unknown>) {
return (
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Icon
{...knobsAsProps(props)}
name='chevron-right'
size='xs'
/>
<Icon
{...knobsAsProps(props)}
name='chevron-right'
Expand Down
20 changes: 16 additions & 4 deletions packages/swingset/src/stories/input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/** @jsxImportSource @emotion/react */
import type { InputProps } from '@clerk/ui/mosaic/components/input';
import { Input, inputRecipe } from '@clerk/ui/mosaic/components/input';
import { Input } from '@clerk/ui/mosaic/components/input';

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

Expand All @@ -11,8 +10,16 @@ export { default as __source } from './input.stories?raw';
export const meta: StoryMeta = {
group: 'Components',
title: 'Input',
source: 'packages/ui/src/mosaic/components/input.tsx',
styles: inputRecipe,
source: 'packages/ui/src/mosaic/components/input/input.tsx',
styleEngine: 'stylex',
styles: {
_variants: {
size: { sm: {}, md: {}, lg: {} },
},
_defaultVariants: {
size: 'md',
},
},
};

function knobsAsProps(props: Record<string, unknown>) {
Expand Down Expand Up @@ -41,6 +48,11 @@ export function Sizes(props: Record<string, unknown>) {
size='md'
placeholder='Medium'
/>
<Input
{...knobsAsProps(props)}
size='lg'
placeholder='Large'
/>
</div>
);
}
Expand Down
62 changes: 62 additions & 0 deletions packages/swingset/src/stories/user-profile-profile-panel.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as UserProfileProfilePanelStories from './user-profile-profile-panel.stories';

# UserProfileProfilePanel

The Profile panel from UserProfile, composed without the surrounding navigation shell. The
presentational view renders account details, connected accounts, Web3 wallets, and the danger zone from props
while its callbacks provide the seam for connected Clerk resources.

## Example

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

## Usage

```tsx
import { UserProfileProfilePanelView } from '@clerk/ui/mosaic/user-profile/user-profile-profile-panel.view';

<UserProfileProfilePanelView
imageUrl={user.imageUrl}
name={user.fullName ?? ''}
username={user.username ?? ''}
emails={user.emailAddresses.map(email => ({
id: email.id,
value: email.emailAddress,
isDefault: email.id === user.primaryEmailAddressId,
isVerified: email.verification.status === 'verified',
}))}
phones={user.phoneNumbers.map(phone => ({
id: phone.id,
value: phone.phoneNumber,
isDefault: phone.id === user.primaryPhoneNumberId,
isVerified: phone.verification.status === 'verified',
}))}
connectedAccounts={user.externalAccounts.map(account => ({
id: account.id,
provider: account.provider,
identifier: account.emailAddress,
connected: true,
}))}
web3Wallets={user.web3Wallets.map(wallet => ({
id: wallet.id,
address: wallet.web3Wallet,
isPrimary: wallet.id === user.primaryWeb3WalletId,
isVerified: wallet.verification.status === 'verified',
}))}
onNameChange={setName}
onUsernameChange={setUsername}
onVerifyEmail={verifyEmail}
onSetPrimaryEmail={setPrimaryEmail}
onRemoveEmail={removeEmail}
onVerifyPhone={verifyPhone}
onSetPrimaryPhone={setPrimaryPhone}
onRemovePhone={removePhone}
onRemoveConnectedAccount={removeConnectedAccount}
onAddWeb3Wallet={addWeb3Wallet}
onSetPrimaryWeb3Wallet={setPrimaryWeb3Wallet}
onRemoveWeb3Wallet={removeWeb3Wallet}
/>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/** @jsxImportSource @emotion/react */
import { UserProfileProfilePanelView } from '@clerk/ui/mosaic/user-profile/user-profile-profile-panel.view';
import { useState } from 'react';

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

const providerIconUrl = (provider: string) => `https://img.clerk.com/static/${provider}.svg`;
const profileImageUrl = 'https://avatars.githubusercontent.com/u/51144033?v=4';

export { default as __source } from './user-profile-profile-panel.stories?raw';

export const meta: StoryMeta = {
group: 'User',
title: 'UserProfileProfilePanel',
source: 'packages/ui/src/mosaic/user-profile/user-profile-profile-panel.view.tsx',
};

export function Default(_args: Record<string, unknown>) {
const [name, setName] = useState('Preston Booth');
const [username, setUsername] = useState('prestonxyz');

return (
<UserProfileProfilePanelView
emails={[
{ id: 'email_1', value: 'item1@clerk.dev', isDefault: true, isVerified: true },
{ id: 'email_2', value: 'item2@clerk.dev', isVerified: true },
]}
connectedAccounts={[
{
id: 'google',
provider: 'Google',
identifier: 'test@google.com',
iconUrl: providerIconUrl('google'),
connected: true,
},
{ id: 'apple', provider: 'Apple', iconUrl: providerIconUrl('apple'), connected: false },
]}
web3Wallets={[
{
id: 'metamask',
address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
provider: 'MetaMask',
iconUrl: providerIconUrl('metamask'),
isPrimary: true,
isVerified: true,
},
]}
imageUrl={profileImageUrl}
name={name}
phones={[{ id: 'phone_1', value: '+1 801-888-8181', isDefault: true, isVerified: true }]}
username={username}
onAddEmail={() => undefined}
onAddPhone={() => undefined}
onConnectAccount={() => undefined}
onDeleteAccount={() => undefined}
onEditProfilePicture={() => undefined}
onRemoveConnectedAccount={() => undefined}
onRemoveEmail={() => undefined}
onRemovePhone={() => undefined}
onAddWeb3Wallet={() => undefined}
onRemoveWeb3Wallet={() => undefined}
onSetPrimaryWeb3Wallet={() => undefined}
onSetPrimaryEmail={() => undefined}
onSetPrimaryPhone={() => undefined}
onVerifyEmail={() => undefined}
onVerifyPhone={() => undefined}
onNameChange={setName}
onUsernameChange={setUsername}
/>
);
}
4 changes: 1 addition & 3 deletions packages/ui/src/mosaic/block/destructive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,7 @@ export function Destructive({
value={confirmValue}
onChange={e => setConfirmValue(e.target.value)}
disabled={isDeleting}
sx={t => ({
marginBlockStart: t.spacing(1),
})}
style={{ marginBlockStart: 'var(--cl-spacing)' }}
/>
</Box>
<Box
Expand Down
Loading