Skip to content

refactor(ui): declare the whole Mosaic UserButton surface in one table - #9411

Merged
alexcarpenter merged 5 commits into
mainfrom
carp/mosaic-view-frederick-feedback
Aug 12, 2026
Merged

refactor(ui): declare the whole Mosaic UserButton surface in one table#9411
alexcarpenter merged 5 commits into
mainfrom
carp/mosaic-view-frederick-feedback

Conversation

@alexcarpenter

@alexcarpenter alexcarpenter commented Aug 12, 2026

Copy link
Copy Markdown
Member

Description

feedback follow up from #9184

The Mosaic UserButton popup decided its shape in two places. user-button.layout.ts held two tables of opposite orientation (one keyed by mode, one keyed by affordance) plus a conditional type cross-checking them, and the view still hardcoded the header gear and the active-account row menu on its own.

Each mode now declares its whole surface in one table, top to bottom:

combined: {
  header: ['inviteMembers', 'manageLead'],
  organizations: { heading: ['createOrganization', 'manageAccount', 'signOut'] },
  sessions: { heading: ['addAccount'] },
  footer: ['signOutAll'],
},

A section the mode does not carry is false, so it has no array to hold actions and placing one there is impossible to express. That removed the 'none' slot and the ModeSlot<M> conditional type outright.

resolveUserButtonLayout() runs the table against the data once and hands back which sections render and what each slot carries, in order. Every consumer loops its own list rather than asking where a given action landed:

for (const action of data.layout.actions.header) {
  if (action === 'manageLead') { /* gear: the org where one leads, else the account */ }
}

Behaviour is unchanged in every reachable state. The ASCII diagram at the top of the file shows the three modes side by side.

Renames follow the data contract, so workspace now means only the personal-or-organization union: ActiveAccountRow -> OrganizationsHeading, AccountsSection -> SessionSection, WorkspaceRow -> SwitcherRow, and so on.

This is internal to @clerk/ui; the Mosaic UserButton is not exported yet, hence the empty changeset.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Every mode now describes its own surface top to bottom in a single table, so
the header gear and the row menus read where they land instead of hardcoding it.
@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 889cd73

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When 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

@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview Aug 12, 2026 1:31pm
swingset Ready Ready Preview Aug 12, 2026 1:31pm

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Aug 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9411

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9411

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9411

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9411

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9411

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9411

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9411

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9411

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@9411

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9411

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9411

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9411

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9411

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9411

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9411

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9411

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9411

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9411

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9411

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9411

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9411

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9411

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9411

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9411

commit: 889cd73

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 9715c970-3f42-468b-a770-1e3c27dc9600

📥 Commits

Reviewing files that changed from the base of the PR and between 9d5abd4 and 889cd73.

📒 Files selected for processing (1)
  • packages/ui/src/mosaic/user-button/user-button.layout.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)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui/src/mosaic/user-button/user-button.layout.ts

📝 Walkthrough

Walkthrough

The UserButton layout resolver now uses mode-defined action slots for headers, headings, and footers. It separately controls organization and session visibility and filters unavailable actions. The view renders organization and session sections from the resolved layout and uses session-oriented component names. Tests cover placement, visibility, availability, and lead-surface priority. An empty Changeset file was added.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the UserButton layout refactor, its implementation, behavior, and testing status.
Title check ✅ Passed The title clearly summarizes the main refactor: defining the complete Mosaic UserButton surface in one table.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-12T13:34:23.310Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 1
🔴 Breaking changes 8
🟡 Non-breaking changes 0
🟢 Additions 1

Warning
8 breaking change(s) detected - Major version bump required

🤖 This report was reviewed by claude-sonnet-4-6.

🔴 Breaking changes index (8)

Every breaking change, up front. Full diffs are in the package sections below.

Package Subpath Change
@clerk/shared ./types BillingAppliedDiscount.durationInCycles
@clerk/shared ./types BillingAppliedDiscountJSON.duration_in_cycles
@clerk/shared ./types ProtectConfigJSON.tokens_invalid_before
@clerk/shared ./types ProtectConfigResource.id
@clerk/shared ./types ProtectConfigResource.tokens_invalid_before
@clerk/shared ./types ProtectLoader.text_content
@clerk/shared ./types ProtectLoader.token_timeout_ms
@clerk/shared ./types ProtectLoader.token_url

@clerk/shared

Current version: 4.28.1
Recommended bump: MAJOR → 5.0.0

Subpath ./types

🔴 Breaking Changes (8)

Changed: BillingAppliedDiscount.durationInCycles
- durationInCycles?: number | null;

Static analyzer: Removed property BillingAppliedDiscount.durationInCycles

🤖 AI review (confirmed) (90%): Removing an existing optional property durationInCycles from BillingAppliedDiscount breaks consumers that read or reference this property.

Migration: Remove any references to durationInCycles on BillingAppliedDiscount objects in consumer code.

Changed: BillingAppliedDiscountJSON.duration_in_cycles
- duration_in_cycles?: number | null;

Static analyzer: Removed property BillingAppliedDiscountJSON.duration_in_cycles

🤖 AI review (confirmed) (90%): Removing duration_in_cycles from BillingAppliedDiscountJSON breaks consumers that read or reference this property.

Migration: Remove any references to duration_in_cycles on BillingAppliedDiscountJSON objects in consumer code.

Changed: ProtectConfigJSON.tokens_invalid_before
- tokens_invalid_before?: number;

Static analyzer: Removed property ProtectConfigJSON.tokens_invalid_before

🤖 AI review (confirmed) (90%): Removing tokens_invalid_before from ProtectConfigJSON breaks consumers that read or reference this property.

Migration: Remove any references to tokens_invalid_before on ProtectConfigJSON objects in consumer code.

Changed: ProtectConfigResource.id
- id?: string;
+ id: string;

Static analyzer: Breaking change in property ProtectConfigResource.id: Member is no longer optional

🤖 AI review (confirmed) (85%): Making id required on ProtectConfigResource breaks consumers that construct or assign objects of this type without providing id, and also breaks consumers that typed it as potentially undefined.

Migration: Ensure all code constructing or assigning ProtectConfigResource objects provides a non-undefined id string value.

Changed: ProtectConfigResource.tokens_invalid_before
- tokens_invalid_before?: number;

Static analyzer: Removed property ProtectConfigResource.tokens_invalid_before

🤖 AI review (confirmed) (90%): Removing tokens_invalid_before from ProtectConfigResource breaks consumers that read or reference this property.

Migration: Remove any references to tokens_invalid_before on ProtectConfigResource objects in consumer code.

Changed: ProtectLoader.text_content
- text_content?: string;

Static analyzer: Removed property ProtectLoader.text_content

🤖 AI review (confirmed) (90%): Removing text_content from ProtectLoader breaks consumers that read or reference this property.

Migration: Remove any references to text_content on ProtectLoader objects in consumer code.

Changed: ProtectLoader.token_timeout_ms
- token_timeout_ms?: number;

Static analyzer: Removed property ProtectLoader.token_timeout_ms

🤖 AI review (confirmed) (90%): Removing token_timeout_ms from ProtectLoader breaks consumers that read or reference this property.

Migration: Remove any references to token_timeout_ms on ProtectLoader objects in consumer code.

Changed: ProtectLoader.token_url
- token_url?: string;

Static analyzer: Removed property ProtectLoader.token_url

🤖 AI review (confirmed) (90%): Removing token_url from ProtectLoader breaks consumers that read or reference this property.

Migration: Remove any references to token_url on ProtectLoader objects in consumer code.

🟢 Additions (1)

Added: ProtectLoader.textContent
+ textContent?: string;

Added property ProtectLoader.textContent


Report generated by Break Check

Last ran on 889cd73.

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
packages/ui/src/mosaic/user-button/user-button.layout.ts (1)

102-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider readonly arrays in the resolved contract.

actions exposes mutable UserButtonAction[] values, so any consumer can push into the resolved layout. The resolver already builds fresh arrays, so a readonly type costs nothing and keeps the contract immutable.

♻️ Proposed change
   /** What each slot carries, in the order it renders. */
-  actions: Record<UserButtonSlot, UserButtonAction[]>;
+  readonly actions: Readonly<Record<UserButtonSlot, readonly UserButtonAction[]>>;
 }

The internal actions variable in resolveUserButtonLayout can stay Record<UserButtonSlot, UserButtonAction[]>; the return type widens to the readonly shape.

As per coding guidelines: "Verify consistent use of readonly for immutable data in code review" and "Declare readonly arrays and objects for immutable data structures".

🤖 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/user-button/user-button.layout.ts` around lines 102 -
104, Update the resolved layout contract’s actions property to use readonly
UserButtonAction arrays, while keeping the internal actions variable in
resolveUserButtonLayout mutable as currently constructed. Ensure the returned
resolved layout exposes immutable arrays without changing resolver 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.

Nitpick comments:
In `@packages/ui/src/mosaic/user-button/user-button.layout.ts`:
- Around line 102-104: Update the resolved layout contract’s actions property to
use readonly UserButtonAction arrays, while keeping the internal actions
variable in resolveUserButtonLayout mutable as currently constructed. Ensure the
returned resolved layout exposes immutable arrays without changing resolver
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: cc93d315-d84c-4387-951a-7363599a41ce

📥 Commits

Reviewing files that changed from the base of the PR and between 44edcc9 and db76208.

📒 Files selected for processing (4)
  • .changeset/clear-mosaic-user-button-layout.md
  • packages/ui/src/mosaic/user-button/__tests__/user-button.layout.test.ts
  • packages/ui/src/mosaic/user-button/user-button.layout.ts
  • packages/ui/src/mosaic/user-button/user-button.view.tsx
🔗 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)

@Ephem Ephem left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is so much clearer, I ❤️ it! 🙏

footer: readonly UserButtonAction[];
}

const modes = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This could be renamed something like modeDefaults right? Get's merged with data and produces a UserButtonLayout which is the "public" shape outside of this file?

Renaming is very much a NIT, mostly checking my understanding.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Your understanding is exactly right: modes is the static per-mode declaration, resolveUserButtonLayout() runs it against UserButtonData, and UserButtonLayout is the only shape that leaves the file (ListLayout, ModeLayout and modes are all private).

I skipped modeDefaults though, because "defaults" implies something overrides them and nothing does. The data can only drop an action (offered() removes inviteMembers when no org is active) or relocate one (the accounts heading's actions fall to the footer when there is no other account). Nothing ever supplies a competing value that wins, so the name would advertise an override mechanism that isn't there.

But you were pointing at something real. The collision wasn't the table, it was that ModeLayout (static input) and UserButtonLayout (resolved output) both say "layout" — and inside the resolver there was a local literally named layout that was not what the function returns:

const layout: ModeLayout = modes[mode];  // not the returned layout

Renamed that to declared, so UserButtonLayout is now the only "layout" in the file. Thanks for the poke.

Comment thread packages/ui/src/mosaic/user-button/user-button.view.tsx Outdated
Comment on lines 738 to 740
{/* Memberships, invitations and suggestions are three separate requests landing at three
different moments. Rendering each as it arrives walks the list in in stages, so the
placeholder stands in for all of them until the last one is in. */}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tangent: Oh how I want suspense. 🙃

Co-authored-by: Fredrik Höglund <fredrik@clerk.dev>
@alexcarpenter
alexcarpenter merged commit 434ca94 into main Aug 12, 2026
52 checks passed
@alexcarpenter
alexcarpenter deleted the carp/mosaic-view-frederick-feedback branch August 12, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants