feat(ui): add UserButton controller - #9185

Merged
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller
Aug 27, 2026
Merged

feat(ui): add UserButton controller#9185
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

The PR #9191 was folded into this one for a final review. See that PR description for additional details.

Stacked on #9184. Connects the UserButton view to live Clerk data.

useUserButtonController() returns a 'loading' | 'hidden' | 'ready' union. When it is ready it carries the view's data contract and the callback behind every row. user-button.tsx is the connected container that owns the popover.

Where the data comes from:

  • activeSession from useUser() and useSession(). The name follows Clerk's own order: first and last name, then username, then the identifier.
  • activeOrganization from useOrganization(), where null is the personal workspace.
  • memberships, suggestions, and invitations from useOrganizationList(), paged as the list scrolls.
  • hasOrganizations from the user resource, so it can answer before those lists load.
  • additionalSessions from the client, without the active one.
  • The membership request count only on the active organization row, and only with org:sys_memberships:manage.

What the rows do:

  • Picking a workspace or an account calls setActive. Signing out calls signOut. Invitations and suggestions accept in place and revalidate the list.
  • Manage account, manage organization, create organization, and invite members open Clerk's own modals, portalled out of the popover so they outlive it. Passing a URL routes to a page of your own instead, and that is the whole opt-in: userProfileUrl, organizationProfileUrl, createOrganizationUrl. afterSelectOrganizationUrl and afterSelectPersonalUrl say where picking a workspace lands.
  • Create organization is withheld from a user without the permission to create one. The personal row is withheld where an organization is required, and hidePersonal withholds it by request.
  • customMenuItems reach the menu through the container, and a custom action closes the popover behind whatever it opens.

The button renders nothing until Clerk answers. While it loads, a signed-out visitor and a session still resolving are indistinguishable, so anything rendered then is a button promised to people who are never going to get one. <ClerkLoading> is where an app that knows its own nav puts a placeholder.

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:

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewAug 27, 2026 3:12pm
swingsetReadyReadyPreviewAug 27, 2026 3:12pm

Request Review

@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f61f68

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

@coderabbitai

coderabbitaiBot commented Jul 16, 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
📝 Walkthrough

Walkthrough

Added useUserButtonController with Clerk-backed user, session, organization, invitation, and suggestion state. Added navigation, modal, switching, sign-out, authorization, pagination, and acceptance handlers. Added the client-side UserButton component with pending-state and action guards. Added controller and component tests, plus changeset metadata.

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

Merge Risk:⚪ Minimal · up to 76060

The PR connects UserButton actions to live account and organization data; the only remaining concern is limited interaction-test coverage for pending actions and closing after selection. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: …Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly identifies the primary change: adding the UserButton controller. It is concise and directly related to the changeset.
Description check✅ PassedThe description directly explains the UserButton controller, connected container, supported data sources, actions, states, and behavior implemented by the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: 1 unsupported.)


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

@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 8786841 to ba22f92CompareJuly 16, 2026 21:54
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from ba22f92 to c9bdf23CompareJuly 30, 2026 18:38
@alexcarpenteralexcarpenter changed the title feat(ui): add AccountButton controllerfeat(ui): add UserButton controllerJul 30, 2026
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from c9bdf23 to 93aa6f2CompareJuly 31, 2026 13:32
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 93aa6f2 to 4643795CompareAugust 3, 2026 15:08
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4643795 to 4467f3dCompareAugust 3, 2026 15:38
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4467f3d to fbd5c2fCompareAugust 3, 2026 17:09
alexcarpenterand others added 29 commits August 27, 2026 17:09
The connected test drives the real controller against a mocked Clerk, which is what
makes it worth having and also what makes it slow. Cases that only ever asserted what
the popover renders now sit in the view test, leaving the connected one to prove the
layers compose. Also covers `hidePersonal` reaching the popover through the container.
UserButtonProps picked only modePriority off the root, so the connected component was hard-wired to the combined surface and the orgs/user modes were reachable only by composing UserButtonView directly.
Presses a custom row on the connected UserButton and checks the app's
callback runs and the popover closes behind it.
An instance with organizations turned off has none to lead with or list,
so the button is the account's whatever `mode` asked for — `orgs` would
otherwise render an empty shell of a switcher. clerk-js withholds its own
OrganizationSwitcher at the mount boundary, which an app importing this
one never crosses, so the gate lives in the component.
The popover's open state and the one action in flight are the same flow, so
they now live in one machine instead of two useStates. Re-entry, clearing
busy, and closing on success stop being hand-written in the container: RUN is
simply unhandled while busy, and busy is only reachable from open.
Dismissing the popover mid-action now abandons the result rather than letting
it land in a surface that is already gone.
Follows the view's rename of `'orgs'` to `'organization'`.
Also corrects the integration suite's opening comment, which attributed
close-on-success to the container and had the navigation case backwards.
Carries the app's own pages and links into the profile the UserButton opens,
through useCustomPages and the built-in page list it orders them against.
Moves useCustomPages and useUserProfilePages out of the shared mosaic hooks folder into user-button.pages, and routes the custom page order through the same applyOrder rule the menu uses, which also stops a custom page named after a built-in from being sent twice.
Sign up for freeto 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

@alexcarpenter@Ephem
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(ui): add UserButton controller - #9185

Merged
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller
Aug 27, 2026
Merged

feat(ui): add UserButton controller#9185
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

The PR #9191 was folded into this one for a final review. See that PR description for additional details.

Stacked on #9184. Connects the UserButton view to live Clerk data.

useUserButtonController() returns a 'loading' | 'hidden' | 'ready' union. When it is ready it carries the view's data contract and the callback behind every row. user-button.tsx is the connected container that owns the popover.

Where the data comes from:

  • activeSession from useUser() and useSession(). The name follows Clerk's own order: first and last name, then username, then the identifier.
  • activeOrganization from useOrganization(), where null is the personal workspace.
  • memberships, suggestions, and invitations from useOrganizationList(), paged as the list scrolls.
  • hasOrganizations from the user resource, so it can answer before those lists load.
  • additionalSessions from the client, without the active one.
  • The membership request count only on the active organization row, and only with org:sys_memberships:manage.

What the rows do:

  • Picking a workspace or an account calls setActive. Signing out calls signOut. Invitations and suggestions accept in place and revalidate the list.
  • Manage account, manage organization, create organization, and invite members open Clerk's own modals, portalled out of the popover so they outlive it. Passing a URL routes to a page of your own instead, and that is the whole opt-in: userProfileUrl, organizationProfileUrl, createOrganizationUrl. afterSelectOrganizationUrl and afterSelectPersonalUrl say where picking a workspace lands.
  • Create organization is withheld from a user without the permission to create one. The personal row is withheld where an organization is required, and hidePersonal withholds it by request.
  • customMenuItems reach the menu through the container, and a custom action closes the popover behind whatever it opens.

The button renders nothing until Clerk answers. While it loads, a signed-out visitor and a session still resolving are indistinguishable, so anything rendered then is a button promised to people who are never going to get one. <ClerkLoading> is where an app that knows its own nav puts a placeholder.

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:

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewAug 27, 2026 3:12pm
swingsetReadyReadyPreviewAug 27, 2026 3:12pm

Request Review

@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f61f68

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

@coderabbitai

coderabbitaiBot commented Jul 16, 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
📝 Walkthrough

Walkthrough

Added useUserButtonController with Clerk-backed user, session, organization, invitation, and suggestion state. Added navigation, modal, switching, sign-out, authorization, pagination, and acceptance handlers. Added the client-side UserButton component with pending-state and action guards. Added controller and component tests, plus changeset metadata.

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

Merge Risk:⚪ Minimal · up to 76060

The PR connects UserButton actions to live account and organization data; the only remaining concern is limited interaction-test coverage for pending actions and closing after selection. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: …Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly identifies the primary change: adding the UserButton controller. It is concise and directly related to the changeset.
Description check✅ PassedThe description directly explains the UserButton controller, connected container, supported data sources, actions, states, and behavior implemented by the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: 1 unsupported.)


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

@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 8786841 to ba22f92CompareJuly 16, 2026 21:54
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from ba22f92 to c9bdf23CompareJuly 30, 2026 18:38
@alexcarpenteralexcarpenter changed the title feat(ui): add AccountButton controllerfeat(ui): add UserButton controllerJul 30, 2026
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from c9bdf23 to 93aa6f2CompareJuly 31, 2026 13:32
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 93aa6f2 to 4643795CompareAugust 3, 2026 15:08
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4643795 to 4467f3dCompareAugust 3, 2026 15:38
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4467f3d to fbd5c2fCompareAugust 3, 2026 17:09
alexcarpenterand others added 29 commits August 27, 2026 17:09
The connected test drives the real controller against a mocked Clerk, which is what
makes it worth having and also what makes it slow. Cases that only ever asserted what
the popover renders now sit in the view test, leaving the connected one to prove the
layers compose. Also covers `hidePersonal` reaching the popover through the container.
UserButtonProps picked only modePriority off the root, so the connected component was hard-wired to the combined surface and the orgs/user modes were reachable only by composing UserButtonView directly.
Presses a custom row on the connected UserButton and checks the app's
callback runs and the popover closes behind it.
An instance with organizations turned off has none to lead with or list,
so the button is the account's whatever `mode` asked for — `orgs` would
otherwise render an empty shell of a switcher. clerk-js withholds its own
OrganizationSwitcher at the mount boundary, which an app importing this
one never crosses, so the gate lives in the component.
The popover's open state and the one action in flight are the same flow, so
they now live in one machine instead of two useStates. Re-entry, clearing
busy, and closing on success stop being hand-written in the container: RUN is
simply unhandled while busy, and busy is only reachable from open.
Dismissing the popover mid-action now abandons the result rather than letting
it land in a surface that is already gone.
Follows the view's rename of `'orgs'` to `'organization'`.
Also corrects the integration suite's opening comment, which attributed
close-on-success to the container and had the navigation case backwards.
Carries the app's own pages and links into the profile the UserButton opens,
through useCustomPages and the built-in page list it orders them against.
Moves useCustomPages and useUserProfilePages out of the shared mosaic hooks folder into user-button.pages, and routes the custom page order through the same applyOrder rule the menu uses, which also stops a custom page named after a built-in from being sent twice.
Sign up for freeto 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

@alexcarpenter@Ephem
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): add UserButton controller - #9185

Merged
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller
Aug 27, 2026
Merged

feat(ui): add UserButton controller#9185
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

The PR #9191 was folded into this one for a final review. See that PR description for additional details.

Stacked on #9184. Connects the UserButton view to live Clerk data.

useUserButtonController() returns a 'loading' | 'hidden' | 'ready' union. When it is ready it carries the view's data contract and the callback behind every row. user-button.tsx is the connected container that owns the popover.

Where the data comes from:

  • activeSession from useUser() and useSession(). The name follows Clerk's own order: first and last name, then username, then the identifier.
  • activeOrganization from useOrganization(), where null is the personal workspace.
  • memberships, suggestions, and invitations from useOrganizationList(), paged as the list scrolls.
  • hasOrganizations from the user resource, so it can answer before those lists load.
  • additionalSessions from the client, without the active one.
  • The membership request count only on the active organization row, and only with org:sys_memberships:manage.

What the rows do:

  • Picking a workspace or an account calls setActive. Signing out calls signOut. Invitations and suggestions accept in place and revalidate the list.
  • Manage account, manage organization, create organization, and invite members open Clerk's own modals, portalled out of the popover so they outlive it. Passing a URL routes to a page of your own instead, and that is the whole opt-in: userProfileUrl, organizationProfileUrl, createOrganizationUrl. afterSelectOrganizationUrl and afterSelectPersonalUrl say where picking a workspace lands.
  • Create organization is withheld from a user without the permission to create one. The personal row is withheld where an organization is required, and hidePersonal withholds it by request.
  • customMenuItems reach the menu through the container, and a custom action closes the popover behind whatever it opens.

The button renders nothing until Clerk answers. While it loads, a signed-out visitor and a session still resolving are indistinguishable, so anything rendered then is a button promised to people who are never going to get one. <ClerkLoading> is where an app that knows its own nav puts a placeholder.

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:

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewAug 27, 2026 3:12pm
swingsetReadyReadyPreviewAug 27, 2026 3:12pm

Request Review

@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f61f68

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

@coderabbitai

coderabbitaiBot commented Jul 16, 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
📝 Walkthrough

Walkthrough

Added useUserButtonController with Clerk-backed user, session, organization, invitation, and suggestion state. Added navigation, modal, switching, sign-out, authorization, pagination, and acceptance handlers. Added the client-side UserButton component with pending-state and action guards. Added controller and component tests, plus changeset metadata.

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

Merge Risk:⚪ Minimal · up to 76060

The PR connects UserButton actions to live account and organization data; the only remaining concern is limited interaction-test coverage for pending actions and closing after selection. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: …Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly identifies the primary change: adding the UserButton controller. It is concise and directly related to the changeset.
Description check✅ PassedThe description directly explains the UserButton controller, connected container, supported data sources, actions, states, and behavior implemented by the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: 1 unsupported.)


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

@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 8786841 to ba22f92CompareJuly 16, 2026 21:54
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from ba22f92 to c9bdf23CompareJuly 30, 2026 18:38
@alexcarpenteralexcarpenter changed the title feat(ui): add AccountButton controllerfeat(ui): add UserButton controllerJul 30, 2026
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from c9bdf23 to 93aa6f2CompareJuly 31, 2026 13:32
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 93aa6f2 to 4643795CompareAugust 3, 2026 15:08
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4643795 to 4467f3dCompareAugust 3, 2026 15:38
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4467f3d to fbd5c2fCompareAugust 3, 2026 17:09
alexcarpenterand others added 29 commits August 27, 2026 17:09
The connected test drives the real controller against a mocked Clerk, which is what
makes it worth having and also what makes it slow. Cases that only ever asserted what
the popover renders now sit in the view test, leaving the connected one to prove the
layers compose. Also covers `hidePersonal` reaching the popover through the container.
UserButtonProps picked only modePriority off the root, so the connected component was hard-wired to the combined surface and the orgs/user modes were reachable only by composing UserButtonView directly.
Presses a custom row on the connected UserButton and checks the app's
callback runs and the popover closes behind it.
An instance with organizations turned off has none to lead with or list,
so the button is the account's whatever `mode` asked for — `orgs` would
otherwise render an empty shell of a switcher. clerk-js withholds its own
OrganizationSwitcher at the mount boundary, which an app importing this
one never crosses, so the gate lives in the component.
The popover's open state and the one action in flight are the same flow, so
they now live in one machine instead of two useStates. Re-entry, clearing
busy, and closing on success stop being hand-written in the container: RUN is
simply unhandled while busy, and busy is only reachable from open.
Dismissing the popover mid-action now abandons the result rather than letting
it land in a surface that is already gone.
Follows the view's rename of `'orgs'` to `'organization'`.
Also corrects the integration suite's opening comment, which attributed
close-on-success to the container and had the navigation case backwards.
Carries the app's own pages and links into the profile the UserButton opens,
through useCustomPages and the built-in page list it orders them against.
Moves useCustomPages and useUserProfilePages out of the shared mosaic hooks folder into user-button.pages, and routes the custom page order through the same applyOrder rule the menu uses, which also stops a custom page named after a built-in from being sent twice.
Sign up for freeto 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

@alexcarpenter@Ephem
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): add UserButton controller - #9185

Merged
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller
Aug 27, 2026
Merged

feat(ui): add UserButton controller#9185
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

The PR #9191 was folded into this one for a final review. See that PR description for additional details.

Stacked on #9184. Connects the UserButton view to live Clerk data.

useUserButtonController() returns a 'loading' | 'hidden' | 'ready' union. When it is ready it carries the view's data contract and the callback behind every row. user-button.tsx is the connected container that owns the popover.

Where the data comes from:

  • activeSession from useUser() and useSession(). The name follows Clerk's own order: first and last name, then username, then the identifier.
  • activeOrganization from useOrganization(), where null is the personal workspace.
  • memberships, suggestions, and invitations from useOrganizationList(), paged as the list scrolls.
  • hasOrganizations from the user resource, so it can answer before those lists load.
  • additionalSessions from the client, without the active one.
  • The membership request count only on the active organization row, and only with org:sys_memberships:manage.

What the rows do:

  • Picking a workspace or an account calls setActive. Signing out calls signOut. Invitations and suggestions accept in place and revalidate the list.
  • Manage account, manage organization, create organization, and invite members open Clerk's own modals, portalled out of the popover so they outlive it. Passing a URL routes to a page of your own instead, and that is the whole opt-in: userProfileUrl, organizationProfileUrl, createOrganizationUrl. afterSelectOrganizationUrl and afterSelectPersonalUrl say where picking a workspace lands.
  • Create organization is withheld from a user without the permission to create one. The personal row is withheld where an organization is required, and hidePersonal withholds it by request.
  • customMenuItems reach the menu through the container, and a custom action closes the popover behind whatever it opens.

The button renders nothing until Clerk answers. While it loads, a signed-out visitor and a session still resolving are indistinguishable, so anything rendered then is a button promised to people who are never going to get one. <ClerkLoading> is where an app that knows its own nav puts a placeholder.

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:

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewAug 27, 2026 3:12pm
swingsetReadyReadyPreviewAug 27, 2026 3:12pm

Request Review

@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f61f68

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

@coderabbitai

coderabbitaiBot commented Jul 16, 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
📝 Walkthrough

Walkthrough

Added useUserButtonController with Clerk-backed user, session, organization, invitation, and suggestion state. Added navigation, modal, switching, sign-out, authorization, pagination, and acceptance handlers. Added the client-side UserButton component with pending-state and action guards. Added controller and component tests, plus changeset metadata.

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

Merge Risk:⚪ Minimal · up to 76060

The PR connects UserButton actions to live account and organization data; the only remaining concern is limited interaction-test coverage for pending actions and closing after selection. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: …Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly identifies the primary change: adding the UserButton controller. It is concise and directly related to the changeset.
Description check✅ PassedThe description directly explains the UserButton controller, connected container, supported data sources, actions, states, and behavior implemented by the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: 1 unsupported.)


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

@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 8786841 to ba22f92CompareJuly 16, 2026 21:54
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from ba22f92 to c9bdf23CompareJuly 30, 2026 18:38
@alexcarpenteralexcarpenter changed the title feat(ui): add AccountButton controllerfeat(ui): add UserButton controllerJul 30, 2026
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from c9bdf23 to 93aa6f2CompareJuly 31, 2026 13:32
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 93aa6f2 to 4643795CompareAugust 3, 2026 15:08
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4643795 to 4467f3dCompareAugust 3, 2026 15:38
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4467f3d to fbd5c2fCompareAugust 3, 2026 17:09
alexcarpenterand others added 29 commits August 27, 2026 17:09
The connected test drives the real controller against a mocked Clerk, which is what
makes it worth having and also what makes it slow. Cases that only ever asserted what
the popover renders now sit in the view test, leaving the connected one to prove the
layers compose. Also covers `hidePersonal` reaching the popover through the container.
UserButtonProps picked only modePriority off the root, so the connected component was hard-wired to the combined surface and the orgs/user modes were reachable only by composing UserButtonView directly.
Presses a custom row on the connected UserButton and checks the app's
callback runs and the popover closes behind it.
An instance with organizations turned off has none to lead with or list,
so the button is the account's whatever `mode` asked for — `orgs` would
otherwise render an empty shell of a switcher. clerk-js withholds its own
OrganizationSwitcher at the mount boundary, which an app importing this
one never crosses, so the gate lives in the component.
The popover's open state and the one action in flight are the same flow, so
they now live in one machine instead of two useStates. Re-entry, clearing
busy, and closing on success stop being hand-written in the container: RUN is
simply unhandled while busy, and busy is only reachable from open.
Dismissing the popover mid-action now abandons the result rather than letting
it land in a surface that is already gone.
Follows the view's rename of `'orgs'` to `'organization'`.
Also corrects the integration suite's opening comment, which attributed
close-on-success to the container and had the navigation case backwards.
Carries the app's own pages and links into the profile the UserButton opens,
through useCustomPages and the built-in page list it orders them against.
Moves useCustomPages and useUserProfilePages out of the shared mosaic hooks folder into user-button.pages, and routes the custom page order through the same applyOrder rule the menu uses, which also stops a custom page named after a built-in from being sent twice.
Sign up for freeto 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

@alexcarpenter@Ephem
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(ui): add UserButton controller - #9185

Merged
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller
Aug 27, 2026
Merged

feat(ui): add UserButton controller#9185
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

The PR #9191 was folded into this one for a final review. See that PR description for additional details.

Stacked on #9184. Connects the UserButton view to live Clerk data.

useUserButtonController() returns a 'loading' | 'hidden' | 'ready' union. When it is ready it carries the view's data contract and the callback behind every row. user-button.tsx is the connected container that owns the popover.

Where the data comes from:

  • activeSession from useUser() and useSession(). The name follows Clerk's own order: first and last name, then username, then the identifier.
  • activeOrganization from useOrganization(), where null is the personal workspace.
  • memberships, suggestions, and invitations from useOrganizationList(), paged as the list scrolls.
  • hasOrganizations from the user resource, so it can answer before those lists load.
  • additionalSessions from the client, without the active one.
  • The membership request count only on the active organization row, and only with org:sys_memberships:manage.

What the rows do:

  • Picking a workspace or an account calls setActive. Signing out calls signOut. Invitations and suggestions accept in place and revalidate the list.
  • Manage account, manage organization, create organization, and invite members open Clerk's own modals, portalled out of the popover so they outlive it. Passing a URL routes to a page of your own instead, and that is the whole opt-in: userProfileUrl, organizationProfileUrl, createOrganizationUrl. afterSelectOrganizationUrl and afterSelectPersonalUrl say where picking a workspace lands.
  • Create organization is withheld from a user without the permission to create one. The personal row is withheld where an organization is required, and hidePersonal withholds it by request.
  • customMenuItems reach the menu through the container, and a custom action closes the popover behind whatever it opens.

The button renders nothing until Clerk answers. While it loads, a signed-out visitor and a session still resolving are indistinguishable, so anything rendered then is a button promised to people who are never going to get one. <ClerkLoading> is where an app that knows its own nav puts a placeholder.

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:

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewAug 27, 2026 3:12pm
swingsetReadyReadyPreviewAug 27, 2026 3:12pm

Request Review

@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f61f68

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

@coderabbitai

coderabbitaiBot commented Jul 16, 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
📝 Walkthrough

Walkthrough

Added useUserButtonController with Clerk-backed user, session, organization, invitation, and suggestion state. Added navigation, modal, switching, sign-out, authorization, pagination, and acceptance handlers. Added the client-side UserButton component with pending-state and action guards. Added controller and component tests, plus changeset metadata.

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

Merge Risk:⚪ Minimal · up to 76060

The PR connects UserButton actions to live account and organization data; the only remaining concern is limited interaction-test coverage for pending actions and closing after selection. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: …Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly identifies the primary change: adding the UserButton controller. It is concise and directly related to the changeset.
Description check✅ PassedThe description directly explains the UserButton controller, connected container, supported data sources, actions, states, and behavior implemented by the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: 1 unsupported.)


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

@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 8786841 to ba22f92CompareJuly 16, 2026 21:54
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from ba22f92 to c9bdf23CompareJuly 30, 2026 18:38
@alexcarpenteralexcarpenter changed the title feat(ui): add AccountButton controllerfeat(ui): add UserButton controllerJul 30, 2026
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from c9bdf23 to 93aa6f2CompareJuly 31, 2026 13:32
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 93aa6f2 to 4643795CompareAugust 3, 2026 15:08
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4643795 to 4467f3dCompareAugust 3, 2026 15:38
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4467f3d to fbd5c2fCompareAugust 3, 2026 17:09
alexcarpenterand others added 29 commits August 27, 2026 17:09
The connected test drives the real controller against a mocked Clerk, which is what
makes it worth having and also what makes it slow. Cases that only ever asserted what
the popover renders now sit in the view test, leaving the connected one to prove the
layers compose. Also covers `hidePersonal` reaching the popover through the container.
UserButtonProps picked only modePriority off the root, so the connected component was hard-wired to the combined surface and the orgs/user modes were reachable only by composing UserButtonView directly.
Presses a custom row on the connected UserButton and checks the app's
callback runs and the popover closes behind it.
An instance with organizations turned off has none to lead with or list,
so the button is the account's whatever `mode` asked for — `orgs` would
otherwise render an empty shell of a switcher. clerk-js withholds its own
OrganizationSwitcher at the mount boundary, which an app importing this
one never crosses, so the gate lives in the component.
The popover's open state and the one action in flight are the same flow, so
they now live in one machine instead of two useStates. Re-entry, clearing
busy, and closing on success stop being hand-written in the container: RUN is
simply unhandled while busy, and busy is only reachable from open.
Dismissing the popover mid-action now abandons the result rather than letting
it land in a surface that is already gone.
Follows the view's rename of `'orgs'` to `'organization'`.
Also corrects the integration suite's opening comment, which attributed
close-on-success to the container and had the navigation case backwards.
Carries the app's own pages and links into the profile the UserButton opens,
through useCustomPages and the built-in page list it orders them against.
Moves useCustomPages and useUserProfilePages out of the shared mosaic hooks folder into user-button.pages, and routes the custom page order through the same applyOrder rule the menu uses, which also stops a custom page named after a built-in from being sent twice.
Sign up for freeto 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

@alexcarpenter@Ephem
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): add UserButton controller - #9185

Merged
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller
Aug 27, 2026
Merged

feat(ui): add UserButton controller#9185
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

The PR #9191 was folded into this one for a final review. See that PR description for additional details.

Stacked on #9184. Connects the UserButton view to live Clerk data.

useUserButtonController() returns a 'loading' | 'hidden' | 'ready' union. When it is ready it carries the view's data contract and the callback behind every row. user-button.tsx is the connected container that owns the popover.

Where the data comes from:

  • activeSession from useUser() and useSession(). The name follows Clerk's own order: first and last name, then username, then the identifier.
  • activeOrganization from useOrganization(), where null is the personal workspace.
  • memberships, suggestions, and invitations from useOrganizationList(), paged as the list scrolls.
  • hasOrganizations from the user resource, so it can answer before those lists load.
  • additionalSessions from the client, without the active one.
  • The membership request count only on the active organization row, and only with org:sys_memberships:manage.

What the rows do:

  • Picking a workspace or an account calls setActive. Signing out calls signOut. Invitations and suggestions accept in place and revalidate the list.
  • Manage account, manage organization, create organization, and invite members open Clerk's own modals, portalled out of the popover so they outlive it. Passing a URL routes to a page of your own instead, and that is the whole opt-in: userProfileUrl, organizationProfileUrl, createOrganizationUrl. afterSelectOrganizationUrl and afterSelectPersonalUrl say where picking a workspace lands.
  • Create organization is withheld from a user without the permission to create one. The personal row is withheld where an organization is required, and hidePersonal withholds it by request.
  • customMenuItems reach the menu through the container, and a custom action closes the popover behind whatever it opens.

The button renders nothing until Clerk answers. While it loads, a signed-out visitor and a session still resolving are indistinguishable, so anything rendered then is a button promised to people who are never going to get one. <ClerkLoading> is where an app that knows its own nav puts a placeholder.

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:

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewAug 27, 2026 3:12pm
swingsetReadyReadyPreviewAug 27, 2026 3:12pm

Request Review

@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f61f68

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

@coderabbitai

coderabbitaiBot commented Jul 16, 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
📝 Walkthrough

Walkthrough

Added useUserButtonController with Clerk-backed user, session, organization, invitation, and suggestion state. Added navigation, modal, switching, sign-out, authorization, pagination, and acceptance handlers. Added the client-side UserButton component with pending-state and action guards. Added controller and component tests, plus changeset metadata.

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

Merge Risk:⚪ Minimal · up to 76060

The PR connects UserButton actions to live account and organization data; the only remaining concern is limited interaction-test coverage for pending actions and closing after selection. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: …Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly identifies the primary change: adding the UserButton controller. It is concise and directly related to the changeset.
Description check✅ PassedThe description directly explains the UserButton controller, connected container, supported data sources, actions, states, and behavior implemented by the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: 1 unsupported.)


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

@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 8786841 to ba22f92CompareJuly 16, 2026 21:54
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from ba22f92 to c9bdf23CompareJuly 30, 2026 18:38
@alexcarpenteralexcarpenter changed the title feat(ui): add AccountButton controllerfeat(ui): add UserButton controllerJul 30, 2026
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from c9bdf23 to 93aa6f2CompareJuly 31, 2026 13:32
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 93aa6f2 to 4643795CompareAugust 3, 2026 15:08
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4643795 to 4467f3dCompareAugust 3, 2026 15:38
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4467f3d to fbd5c2fCompareAugust 3, 2026 17:09
alexcarpenterand others added 29 commits August 27, 2026 17:09
The connected test drives the real controller against a mocked Clerk, which is what
makes it worth having and also what makes it slow. Cases that only ever asserted what
the popover renders now sit in the view test, leaving the connected one to prove the
layers compose. Also covers `hidePersonal` reaching the popover through the container.
UserButtonProps picked only modePriority off the root, so the connected component was hard-wired to the combined surface and the orgs/user modes were reachable only by composing UserButtonView directly.
Presses a custom row on the connected UserButton and checks the app's
callback runs and the popover closes behind it.
An instance with organizations turned off has none to lead with or list,
so the button is the account's whatever `mode` asked for — `orgs` would
otherwise render an empty shell of a switcher. clerk-js withholds its own
OrganizationSwitcher at the mount boundary, which an app importing this
one never crosses, so the gate lives in the component.
The popover's open state and the one action in flight are the same flow, so
they now live in one machine instead of two useStates. Re-entry, clearing
busy, and closing on success stop being hand-written in the container: RUN is
simply unhandled while busy, and busy is only reachable from open.
Dismissing the popover mid-action now abandons the result rather than letting
it land in a surface that is already gone.
Follows the view's rename of `'orgs'` to `'organization'`.
Also corrects the integration suite's opening comment, which attributed
close-on-success to the container and had the navigation case backwards.
Carries the app's own pages and links into the profile the UserButton opens,
through useCustomPages and the built-in page list it orders them against.
Moves useCustomPages and useUserProfilePages out of the shared mosaic hooks folder into user-button.pages, and routes the custom page order through the same applyOrder rule the menu uses, which also stops a custom page named after a built-in from being sent twice.
Sign up for freeto 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

@alexcarpenter@Ephem
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): add UserButton controller - #9185

Merged
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller
Aug 27, 2026
Merged

feat(ui): add UserButton controller#9185
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

The PR #9191 was folded into this one for a final review. See that PR description for additional details.

Stacked on #9184. Connects the UserButton view to live Clerk data.

useUserButtonController() returns a 'loading' | 'hidden' | 'ready' union. When it is ready it carries the view's data contract and the callback behind every row. user-button.tsx is the connected container that owns the popover.

Where the data comes from:

  • activeSession from useUser() and useSession(). The name follows Clerk's own order: first and last name, then username, then the identifier.
  • activeOrganization from useOrganization(), where null is the personal workspace.
  • memberships, suggestions, and invitations from useOrganizationList(), paged as the list scrolls.
  • hasOrganizations from the user resource, so it can answer before those lists load.
  • additionalSessions from the client, without the active one.
  • The membership request count only on the active organization row, and only with org:sys_memberships:manage.

What the rows do:

  • Picking a workspace or an account calls setActive. Signing out calls signOut. Invitations and suggestions accept in place and revalidate the list.
  • Manage account, manage organization, create organization, and invite members open Clerk's own modals, portalled out of the popover so they outlive it. Passing a URL routes to a page of your own instead, and that is the whole opt-in: userProfileUrl, organizationProfileUrl, createOrganizationUrl. afterSelectOrganizationUrl and afterSelectPersonalUrl say where picking a workspace lands.
  • Create organization is withheld from a user without the permission to create one. The personal row is withheld where an organization is required, and hidePersonal withholds it by request.
  • customMenuItems reach the menu through the container, and a custom action closes the popover behind whatever it opens.

The button renders nothing until Clerk answers. While it loads, a signed-out visitor and a session still resolving are indistinguishable, so anything rendered then is a button promised to people who are never going to get one. <ClerkLoading> is where an app that knows its own nav puts a placeholder.

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:

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewAug 27, 2026 3:12pm
swingsetReadyReadyPreviewAug 27, 2026 3:12pm

Request Review

@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f61f68

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

@coderabbitai

coderabbitaiBot commented Jul 16, 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
📝 Walkthrough

Walkthrough

Added useUserButtonController with Clerk-backed user, session, organization, invitation, and suggestion state. Added navigation, modal, switching, sign-out, authorization, pagination, and acceptance handlers. Added the client-side UserButton component with pending-state and action guards. Added controller and component tests, plus changeset metadata.

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

Merge Risk:⚪ Minimal · up to 76060

The PR connects UserButton actions to live account and organization data; the only remaining concern is limited interaction-test coverage for pending actions and closing after selection. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: …Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly identifies the primary change: adding the UserButton controller. It is concise and directly related to the changeset.
Description check✅ PassedThe description directly explains the UserButton controller, connected container, supported data sources, actions, states, and behavior implemented by the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: 1 unsupported.)


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

@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 8786841 to ba22f92CompareJuly 16, 2026 21:54
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from ba22f92 to c9bdf23CompareJuly 30, 2026 18:38
@alexcarpenteralexcarpenter changed the title feat(ui): add AccountButton controllerfeat(ui): add UserButton controllerJul 30, 2026
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from c9bdf23 to 93aa6f2CompareJuly 31, 2026 13:32
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 93aa6f2 to 4643795CompareAugust 3, 2026 15:08
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4643795 to 4467f3dCompareAugust 3, 2026 15:38
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4467f3d to fbd5c2fCompareAugust 3, 2026 17:09
alexcarpenterand others added 29 commits August 27, 2026 17:09
The connected test drives the real controller against a mocked Clerk, which is what
makes it worth having and also what makes it slow. Cases that only ever asserted what
the popover renders now sit in the view test, leaving the connected one to prove the
layers compose. Also covers `hidePersonal` reaching the popover through the container.
UserButtonProps picked only modePriority off the root, so the connected component was hard-wired to the combined surface and the orgs/user modes were reachable only by composing UserButtonView directly.
Presses a custom row on the connected UserButton and checks the app's
callback runs and the popover closes behind it.
An instance with organizations turned off has none to lead with or list,
so the button is the account's whatever `mode` asked for — `orgs` would
otherwise render an empty shell of a switcher. clerk-js withholds its own
OrganizationSwitcher at the mount boundary, which an app importing this
one never crosses, so the gate lives in the component.
The popover's open state and the one action in flight are the same flow, so
they now live in one machine instead of two useStates. Re-entry, clearing
busy, and closing on success stop being hand-written in the container: RUN is
simply unhandled while busy, and busy is only reachable from open.
Dismissing the popover mid-action now abandons the result rather than letting
it land in a surface that is already gone.
Follows the view's rename of `'orgs'` to `'organization'`.
Also corrects the integration suite's opening comment, which attributed
close-on-success to the container and had the navigation case backwards.
Carries the app's own pages and links into the profile the UserButton opens,
through useCustomPages and the built-in page list it orders them against.
Moves useCustomPages and useUserProfilePages out of the shared mosaic hooks folder into user-button.pages, and routes the custom page order through the same applyOrder rule the menu uses, which also stops a custom page named after a built-in from being sent twice.
Sign up for freeto 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

@alexcarpenter@Ephem
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(ui): add UserButton controller - #9185

Merged
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller
Aug 27, 2026
Merged

feat(ui): add UserButton controller#9185
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

The PR #9191 was folded into this one for a final review. See that PR description for additional details.

Stacked on #9184. Connects the UserButton view to live Clerk data.

useUserButtonController() returns a 'loading' | 'hidden' | 'ready' union. When it is ready it carries the view's data contract and the callback behind every row. user-button.tsx is the connected container that owns the popover.

Where the data comes from:

  • activeSession from useUser() and useSession(). The name follows Clerk's own order: first and last name, then username, then the identifier.
  • activeOrganization from useOrganization(), where null is the personal workspace.
  • memberships, suggestions, and invitations from useOrganizationList(), paged as the list scrolls.
  • hasOrganizations from the user resource, so it can answer before those lists load.
  • additionalSessions from the client, without the active one.
  • The membership request count only on the active organization row, and only with org:sys_memberships:manage.

What the rows do:

  • Picking a workspace or an account calls setActive. Signing out calls signOut. Invitations and suggestions accept in place and revalidate the list.
  • Manage account, manage organization, create organization, and invite members open Clerk's own modals, portalled out of the popover so they outlive it. Passing a URL routes to a page of your own instead, and that is the whole opt-in: userProfileUrl, organizationProfileUrl, createOrganizationUrl. afterSelectOrganizationUrl and afterSelectPersonalUrl say where picking a workspace lands.
  • Create organization is withheld from a user without the permission to create one. The personal row is withheld where an organization is required, and hidePersonal withholds it by request.
  • customMenuItems reach the menu through the container, and a custom action closes the popover behind whatever it opens.

The button renders nothing until Clerk answers. While it loads, a signed-out visitor and a session still resolving are indistinguishable, so anything rendered then is a button promised to people who are never going to get one. <ClerkLoading> is where an app that knows its own nav puts a placeholder.

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:

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewAug 27, 2026 3:12pm
swingsetReadyReadyPreviewAug 27, 2026 3:12pm

Request Review

@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f61f68

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

@coderabbitai

coderabbitaiBot commented Jul 16, 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
📝 Walkthrough

Walkthrough

Added useUserButtonController with Clerk-backed user, session, organization, invitation, and suggestion state. Added navigation, modal, switching, sign-out, authorization, pagination, and acceptance handlers. Added the client-side UserButton component with pending-state and action guards. Added controller and component tests, plus changeset metadata.

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

Merge Risk:⚪ Minimal · up to 76060

The PR connects UserButton actions to live account and organization data; the only remaining concern is limited interaction-test coverage for pending actions and closing after selection. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: …Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly identifies the primary change: adding the UserButton controller. It is concise and directly related to the changeset.
Description check✅ PassedThe description directly explains the UserButton controller, connected container, supported data sources, actions, states, and behavior implemented by the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: 1 unsupported.)


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

@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 8786841 to ba22f92CompareJuly 16, 2026 21:54
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from ba22f92 to c9bdf23CompareJuly 30, 2026 18:38
@alexcarpenteralexcarpenter changed the title feat(ui): add AccountButton controllerfeat(ui): add UserButton controllerJul 30, 2026
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from c9bdf23 to 93aa6f2CompareJuly 31, 2026 13:32
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 93aa6f2 to 4643795CompareAugust 3, 2026 15:08
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4643795 to 4467f3dCompareAugust 3, 2026 15:38
@alexcarpenter
alexcarpenterforce-pushed the carp/account-button-controller branch from 4467f3d to fbd5c2fCompareAugust 3, 2026 17:09
alexcarpenterand others added 29 commits August 27, 2026 17:09
The connected test drives the real controller against a mocked Clerk, which is what
makes it worth having and also what makes it slow. Cases that only ever asserted what
the popover renders now sit in the view test, leaving the connected one to prove the
layers compose. Also covers `hidePersonal` reaching the popover through the container.
UserButtonProps picked only modePriority off the root, so the connected component was hard-wired to the combined surface and the orgs/user modes were reachable only by composing UserButtonView directly.
Presses a custom row on the connected UserButton and checks the app's
callback runs and the popover closes behind it.
An instance with organizations turned off has none to lead with or list,
so the button is the account's whatever `mode` asked for — `orgs` would
otherwise render an empty shell of a switcher. clerk-js withholds its own
OrganizationSwitcher at the mount boundary, which an app importing this
one never crosses, so the gate lives in the component.
The popover's open state and the one action in flight are the same flow, so
they now live in one machine instead of two useStates. Re-entry, clearing
busy, and closing on success stop being hand-written in the container: RUN is
simply unhandled while busy, and busy is only reachable from open.
Dismissing the popover mid-action now abandons the result rather than letting
it land in a surface that is already gone.
Follows the view's rename of `'orgs'` to `'organization'`.
Also corrects the integration suite's opening comment, which attributed
close-on-success to the container and had the navigation case backwards.
Carries the app's own pages and links into the profile the UserButton opens,
through useCustomPages and the built-in page list it orders them against.
Moves useCustomPages and useUserProfilePages out of the shared mosaic hooks folder into user-button.pages, and routes the custom page order through the same applyOrder rule the menu uses, which also stops a custom page named after a built-in from being sent twice.
Sign up for freeto 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

@alexcarpenter@Ephem