Skip to content

feat(ui): rebuild the Mosaic Dialog on StyleX and add composition APIs - #9388

Open
maxyinger wants to merge 2 commits into
max/dialog-updatesfrom
max/dialog-updates-stylex
Open

feat(ui): rebuild the Mosaic Dialog on StyleX and add composition APIs#9388
maxyinger wants to merge 2 commits into
max/dialog-updatesfrom
max/dialog-updates-stylex

Conversation

@maxyinger

@maxyinger maxyinger commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Description

Rebuilds the Mosaic Dialog on StyleX — leaving Tabs as the last component on the
Emotion slot-recipe path — and adds Base UI–style composition APIs to both the headless
primitive and the styled component.

Styling and layout. The dialog's rules ship in @clerk/ui/styles.css; style it via the
.cl-dialog-* slot classes or per-part className / style in place of sx. size
becomes prompt / card / panel and moves to Dialog.Root, since the backdrop reads it
too. The gap to the screen edge is a fixed inset at three breakpoints rather than a
percentage of the viewport. A panel clips and carries no padding — its scroll region is
composed inside it from the ScrollArea atoms, which keeps anything anchored to the popup
from scrolling away and makes a sidebar a plain flex row.

Mobile. Below 48rem a prompt becomes a bottom sheet. `Dia
on-screen keyboard, so the sheet rises above it while a card re-centres and a panel shrinks.
The mobile browser's own chrome is tinted to match the scrim — de
rather than shipped as a colour, and reverting exactly on close.

Composition. Dialog.createHandle() drives a dialog from a trigger it is not nested
under. Multiple triggers can share one dialog through id / pay the root's children as a function of the active trigger's payload. initialFocusandfinalFocusonDialog.Popup` take a bool, a ref, or a function

Also. New Dialog.CloseButton; data-nested so stacked scri
--cl-dialog-origin so a dialog scales out of whatever opened it; Button gains xstyle
for composing StyleX styles into its own. Fixes an enter/exit trat
was keyed to a data-cl-starting-style attribute the headless layer does not emit.

TODO:

  • iOS 26+ leaves white bars behind Safari's chrome. The backdrop is position: fixed, which
    no longer covers the region beneath translucent chrome.
  • AlertDialog preset, and close-confirmation built on top of it.
  • Do we actually want the trigger origin animation here? I'm not sure. might play with this a bit

Checklist

Type of change

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

maxyinger and others added 2 commits August 10, 2026 18:08
Moves the dialog off the Emotion slot-recipe engine onto StyleX, leaving
`tabs` as the last component on the old path, and reworks its sizing,
motion and mobile behaviour on top of that.

`size` becomes three named surfaces — `prompt`, `card`, `panel` — and
moves to `Dialog.Root`, since the backdrop reads it too. The gap to the
screen edge is a fixed inset at three breakpoints rather than a
percentage, which is what makes the surround an even frame. A `panel`
clips and carries no padding, so its scroll region is composed inside it
from the ScrollArea atoms; that keeps the close button anchored and makes
a sidebar a plain flex row.

Below 48rem a `prompt` becomes a bottom sheet, and `Dialog.Viewport`
measures the on-screen keyboard so the sheet rises above it while a card
re-centres and a panel shrinks. The chrome of a mobile browser is tinted
to match the scrim, derived from the backdrop rather than shipped as a
colour, refcounted across stacked dialogs and reverting exactly.

Adds `Dialog.CloseButton`, `data-nested` for stacked scrims, and
`--cl-dialog-origin` so a dialog scales out of whatever opened it.

Also fixes a transition that never ran: it was keyed to a
`data-cl-starting-style` attribute the headless layer does not emit, so
dialogs appeared with no animation at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Dialog.createHandle()` returns a handle passed to both a `Dialog.Trigger`
and a `Dialog.Root`, so a trigger drives a dialog it is not nested under.
The handle also exposes imperative `open()` / `close()` / `isOpen`.

Several triggers can share one dialog, each carrying an `id` and a
`payload`, with the root's children as a function of `{ payload }` so one
dialog renders per-trigger content. Everything keyed to "the trigger" now
follows the one actually used — the dialog scales out of it and returns
focus to it — and `triggerId` names the active trigger in controlled mode,
which also gives controlled, trigger-less dialogs the origin-aware open.

`initialFocus` and `finalFocus` on `Dialog.Popup` take `true`, `false`, a
ref, or a function of the interaction type behind the change. Defaults are
unchanged: first tabbable on open, the trigger on close, except after a
pointer-driven dismissal.

Also retunes the dialog for dark mode. The scrim veils rather than darkens
there — light grey at low alpha over a dark page, against black over a
light one — so the two schemes are unrelated colours rather than one
colour at two opacities. The nested scrim stays solved rather than picked,
landing two levels on the same proportional deepening as light. The popup
shadow becomes one three-layer shadow shared by both schemes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: af5f90d

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

This PR includes changesets to release 4 packages
Name Type
@clerk/ui Patch
@clerk/headless Patch
@clerk/chrome-extension Patch
@clerk/swingset Patch

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 11, 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 11, 2026 1:47am
swingset Ready Ready Preview Aug 11, 2026 1:47am

Request Review

@github-actions github-actions Bot added the ui label Aug 11, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/electron

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

@clerk/electron-passkeys

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

@clerk/eslint-plugin

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

@clerk/expo

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

@clerk/expo-google-signin

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: af5f90d

@maxyinger
maxyinger changed the base branch from main to max/dialog-updates August 11, 2026 01:50
@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-11T01:53:52.477Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 4
🔴 Breaking changes 64
🟡 Non-breaking changes 3
🟢 Additions 0

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

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

🔴 Breaking changes index (64)

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

Package Subpath Change
@clerk/clerk-js . Clerk.setProtectAssertion
@clerk/clerk-js ./no-rhc Clerk.setProtectAssertion
@clerk/shared ./types BillingAppliedDiscount
@clerk/shared ./types BillingAppliedDiscount.amount
@clerk/shared ./types BillingAppliedDiscount.amountOff
@clerk/shared ./types BillingAppliedDiscount.cyclesRemaining
@clerk/shared ./types BillingAppliedDiscount.discountId
@clerk/shared ./types BillingAppliedDiscount.effect
@clerk/shared ./types BillingAppliedDiscount.name
@clerk/shared ./types BillingAppliedDiscount.percentOff
@clerk/shared ./types BillingAppliedDiscount.promoCode
@clerk/shared ./types BillingAppliedDiscountJSON
@clerk/shared ./types BillingAppliedDiscountJSON.amount_off
@clerk/shared ./types BillingAppliedDiscountJSON.amount
@clerk/shared ./types BillingAppliedDiscountJSON.cycles_remaining
@clerk/shared ./types BillingAppliedDiscountJSON.discount_id
@clerk/shared ./types BillingAppliedDiscountJSON.effect
@clerk/shared ./types BillingAppliedDiscountJSON.name
@clerk/shared ./types BillingAppliedDiscountJSON.percent_off
@clerk/shared ./types BillingAppliedDiscountJSON.promo_code
@clerk/shared ./types BillingDiscountRedemption
@clerk/shared ./types BillingDiscountRedemption.amount
@clerk/shared ./types BillingDiscountRedemption.amountOff
@clerk/shared ./types BillingDiscountRedemption.cyclesApplied
@clerk/shared ./types BillingDiscountRedemption.cyclesRemaining
@clerk/shared ./types BillingDiscountRedemption.discountId
@clerk/shared ./types BillingDiscountRedemption.effect
@clerk/shared ./types BillingDiscountRedemption.id
@clerk/shared ./types BillingDiscountRedemption.name
@clerk/shared ./types BillingDiscountRedemption.percentOff
@clerk/shared ./types BillingDiscountRedemption.promoCode
@clerk/shared ./types BillingDiscountRedemption.redeemedAt
@clerk/shared ./types BillingDiscountRedemption.redeemedBy
@clerk/shared ./types BillingDiscountRedemption.source
@clerk/shared ./types BillingDiscountRedemption.status
@clerk/shared ./types BillingDiscountRedemption.subscriptionItemId
@clerk/shared ./types BillingDiscountRedemptionJSON
@clerk/shared ./types BillingDiscountRedemptionJSON.amount_off
@clerk/shared ./types BillingDiscountRedemptionJSON.amount
@clerk/shared ./types BillingDiscountRedemptionJSON.cycles_applied
@clerk/shared ./types BillingDiscountRedemptionJSON.cycles_remaining
@clerk/shared ./types BillingDiscountRedemptionJSON.discount_id
@clerk/shared ./types BillingDiscountRedemptionJSON.effect
@clerk/shared ./types BillingDiscountRedemptionJSON.id
@clerk/shared ./types BillingDiscountRedemptionJSON.name
@clerk/shared ./types BillingDiscountRedemptionJSON.object
@clerk/shared ./types BillingDiscountRedemptionJSON.percent_off
@clerk/shared ./types BillingDiscountRedemptionJSON.promo_code
@clerk/shared ./types BillingDiscountRedemptionJSON.redeemed_at
@clerk/shared ./types BillingDiscountRedemptionJSON.redeemed_by
@clerk/shared ./types BillingDiscountRedemptionJSON.source
@clerk/shared ./types BillingDiscountRedemptionJSON.status
@clerk/shared ./types BillingDiscountRedemptionJSON.subscription_item_id
@clerk/shared ./types BillingDiscounts.discount
@clerk/shared ./types BillingDiscountsJSON.discount
@clerk/shared ./types BillingNamespace.updateCheckout
@clerk/shared ./types BillingSubscriptionItemJSON.applied_discount
@clerk/shared ./types BillingSubscriptionItemResource.appliedDiscount
@clerk/shared ./types Clerk.setProtectAssertion
@clerk/shared ./types ClerkOptions
@clerk/shared ./types HandleOAuthCallbackParams
@clerk/shared ./types ProtectAssertion
@clerk/shared ./types ProtectAssertionResolver
@clerk/shared ./types UpdateCheckoutParams

@clerk/shared

Version: 4.28.1 → 4.27.1
Recommended bump: MAJOR

Subpath ./types

🔴 Breaking Changes (62)

Changed: BillingAppliedDiscount
- interface BillingAppliedDiscount

Static analyzer: Removed interface BillingAppliedDiscount

🤖 AI review (confirmed) (95%): The interface BillingAppliedDiscount has been removed from the public API surface, breaking any consumer code that references it.

Migration: Remove all references to BillingAppliedDiscount or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscount.amount
- amount: BillingMoneyAmount;

Static analyzer: Removed property BillingAppliedDiscount.amount

🤖 AI review (confirmed) (95%): Property amount on BillingAppliedDiscount is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscount or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscount.amountOff
- amountOff?: BillingMoneyAmount;

Static analyzer: Removed property BillingAppliedDiscount.amountOff

🤖 AI review (confirmed) (95%): Property amountOff on BillingAppliedDiscount is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscount or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscount.cyclesRemaining
- cyclesRemaining: number | null;

Static analyzer: Removed property BillingAppliedDiscount.cyclesRemaining

🤖 AI review (confirmed) (95%): Property cyclesRemaining on BillingAppliedDiscount is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscount or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscount.discountId
- discountId: string;

Static analyzer: Removed property BillingAppliedDiscount.discountId

🤖 AI review (confirmed) (95%): Property discountId on BillingAppliedDiscount is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscount or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscount.effect
- effect: 'percentage' | 'fixed_amount';

Static analyzer: Removed property BillingAppliedDiscount.effect

🤖 AI review (confirmed) (95%): Property effect on BillingAppliedDiscount is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscount or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscount.name
- name: string;

Static analyzer: Removed property BillingAppliedDiscount.name

🤖 AI review (confirmed) (95%): Property name on BillingAppliedDiscount is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscount or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscount.percentOff
- percentOff?: number;

Static analyzer: Removed property BillingAppliedDiscount.percentOff

🤖 AI review (confirmed) (95%): Property percentOff on BillingAppliedDiscount is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscount or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscount.promoCode
- promoCode?: string;

Static analyzer: Removed property BillingAppliedDiscount.promoCode

🤖 AI review (confirmed) (95%): Property promoCode on BillingAppliedDiscount is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscount or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscountJSON
- interface BillingAppliedDiscountJSON

Static analyzer: Removed interface BillingAppliedDiscountJSON

🤖 AI review (confirmed) (95%): The interface BillingAppliedDiscountJSON has been removed from the public API surface, breaking any consumer code that references it.

Migration: Remove all references to BillingAppliedDiscountJSON or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscountJSON.amount_off
- amount_off?: BillingMoneyAmountJSON;

Static analyzer: Removed property BillingAppliedDiscountJSON.amount_off

🤖 AI review (confirmed) (95%): Property amount_off on BillingAppliedDiscountJSON is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscountJSON or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscountJSON.amount
- amount: BillingMoneyAmountJSON;

Static analyzer: Removed property BillingAppliedDiscountJSON.amount

🤖 AI review (confirmed) (95%): Property amount on BillingAppliedDiscountJSON is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscountJSON or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscountJSON.cycles_remaining
- cycles_remaining: number | null;

Static analyzer: Removed property BillingAppliedDiscountJSON.cycles_remaining

🤖 AI review (confirmed) (95%): Property cycles_remaining on BillingAppliedDiscountJSON is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscountJSON or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscountJSON.discount_id
- discount_id: string;

Static analyzer: Removed property BillingAppliedDiscountJSON.discount_id

🤖 AI review (confirmed) (95%): Property discount_id on BillingAppliedDiscountJSON is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscountJSON or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscountJSON.effect
- effect: 'percentage' | 'fixed_amount';

Static analyzer: Removed property BillingAppliedDiscountJSON.effect

🤖 AI review (confirmed) (95%): Property effect on BillingAppliedDiscountJSON is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscountJSON or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscountJSON.name
- name: string;

Static analyzer: Removed property BillingAppliedDiscountJSON.name

🤖 AI review (confirmed) (95%): Property name on BillingAppliedDiscountJSON is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscountJSON or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscountJSON.percent_off
- percent_off?: number;

Static analyzer: Removed property BillingAppliedDiscountJSON.percent_off

🤖 AI review (confirmed) (95%): Property percent_off on BillingAppliedDiscountJSON is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscountJSON or replace with an equivalent locally defined type matching the old shape.

Changed: BillingAppliedDiscountJSON.promo_code
- promo_code?: string;

Static analyzer: Removed property BillingAppliedDiscountJSON.promo_code

🤖 AI review (confirmed) (95%): Property promo_code on BillingAppliedDiscountJSON is removed along with the interface itself.

Migration: Remove all references to BillingAppliedDiscountJSON or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption
- interface BillingDiscountRedemption

Static analyzer: Removed interface BillingDiscountRedemption

🤖 AI review (confirmed) (95%): The interface BillingDiscountRedemption has been removed from the public API surface, breaking any consumer code that references it.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.amount
- amount?: BillingMoneyAmount;

Static analyzer: Removed property BillingDiscountRedemption.amount

🤖 AI review (confirmed) (95%): Property amount on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.amountOff
- amountOff?: BillingMoneyAmount;

Static analyzer: Removed property BillingDiscountRedemption.amountOff

🤖 AI review (confirmed) (95%): Property amountOff on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.cyclesApplied
- cyclesApplied: number;

Static analyzer: Removed property BillingDiscountRedemption.cyclesApplied

🤖 AI review (confirmed) (95%): Property cyclesApplied on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.cyclesRemaining
- cyclesRemaining: number | null;

Static analyzer: Removed property BillingDiscountRedemption.cyclesRemaining

🤖 AI review (confirmed) (95%): Property cyclesRemaining on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.discountId
- discountId: string;

Static analyzer: Removed property BillingDiscountRedemption.discountId

🤖 AI review (confirmed) (95%): Property discountId on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.effect
- effect?: 'percentage' | 'fixed_amount';

Static analyzer: Removed property BillingDiscountRedemption.effect

🤖 AI review (confirmed) (95%): Property effect on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.id
- id: string;

Static analyzer: Removed property BillingDiscountRedemption.id

🤖 AI review (confirmed) (95%): Property id on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.name
- name: string;

Static analyzer: Removed property BillingDiscountRedemption.name

🤖 AI review (confirmed) (95%): Property name on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.percentOff
- percentOff?: number;

Static analyzer: Removed property BillingDiscountRedemption.percentOff

🤖 AI review (confirmed) (95%): Property percentOff on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.promoCode
- promoCode?: string;

Static analyzer: Removed property BillingDiscountRedemption.promoCode

🤖 AI review (confirmed) (95%): Property promoCode on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.redeemedAt
- redeemedAt: Date;

Static analyzer: Removed property BillingDiscountRedemption.redeemedAt

🤖 AI review (confirmed) (95%): Property redeemedAt on BillingDiscountRedemption is removed along with the interface itself.

Migration: Remove all references to BillingDiscountRedemption or replace with an equivalent locally defined type matching the old shape.

Changed: BillingDiscountRedemption.redeemedBy
- redeemedBy: string | null;

Static analyzer: Removed property BillingDiscountRedemption.redeemedBy

🤖 AI review (confirmed) (85%): The redeemedBy property was removed from BillingDiscountRedemption, breaking consumers that read this field.

Migration: Remove any code that accesses .redeemedBy on BillingDiscountRedemption objects.

Changed: BillingDiscountRedemption.source
- source: 'promotion' | 'manual' | 'promo_code';

Static analyzer: Removed property BillingDiscountRedemption.source

🤖 AI review (confirmed) (85%): The source property was removed from BillingDiscountRedemption, breaking consumers that read this field.

Migration: Remove any code that accesses .source on BillingDiscountRedemption objects.

Changed: BillingDiscountRedemption.status
- status?: 'active' | 'exhausted' | 'removed';

Static analyzer: Removed property BillingDiscountRedemption.status

🤖 AI review (confirmed) (85%): The optional status property was removed from BillingDiscountRedemption, breaking consumers that read this field.

Migration: Remove any code that accesses .status on BillingDiscountRedemption objects.

Changed: BillingDiscountRedemption.subscriptionItemId
- subscriptionItemId: string;

Static analyzer: Removed property BillingDiscountRedemption.subscriptionItemId

🤖 AI review (confirmed) (85%): The subscriptionItemId property was removed from BillingDiscountRedemption, breaking consumers that read this field.

Migration: Remove any code that accesses .subscriptionItemId on BillingDiscountRedemption objects.

Changed: BillingDiscountRedemptionJSON
- interface BillingDiscountRedemptionJSON extends ClerkResourceJSON

Static analyzer: Removed interface BillingDiscountRedemptionJSON

🤖 AI review (confirmed) (90%): The entire BillingDiscountRedemptionJSON interface was removed, breaking consumers that reference this type.

Migration: Remove all references to BillingDiscountRedemptionJSON and update any code that depends on it.

Changed: BillingDiscountRedemptionJSON.amount_off
- amount_off?: BillingMoneyAmountJSON;

Static analyzer: Removed property BillingDiscountRedemptionJSON.amount_off

🤖 AI review (confirmed) (85%): The amount_off property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.amount_off as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.amount
- amount?: BillingMoneyAmountJSON;

Static analyzer: Removed property BillingDiscountRedemptionJSON.amount

🤖 AI review (confirmed) (85%): The amount property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.amount as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.cycles_applied
- cycles_applied: number;

Static analyzer: Removed property BillingDiscountRedemptionJSON.cycles_applied

🤖 AI review (confirmed) (85%): The cycles_applied property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.cycles_applied as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.cycles_remaining
- cycles_remaining: number | null;

Static analyzer: Removed property BillingDiscountRedemptionJSON.cycles_remaining

🤖 AI review (confirmed) (85%): The cycles_remaining property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.cycles_remaining as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.discount_id
- discount_id: string;

Static analyzer: Removed property BillingDiscountRedemptionJSON.discount_id

🤖 AI review (confirmed) (85%): The discount_id property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.discount_id as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.effect
- effect?: 'percentage' | 'fixed_amount';

Static analyzer: Removed property BillingDiscountRedemptionJSON.effect

🤖 AI review (confirmed) (85%): The effect property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.effect as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.id
- id: string;

Static analyzer: Removed property BillingDiscountRedemptionJSON.id

🤖 AI review (confirmed) (85%): The id property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.id as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.name
- name: string;

Static analyzer: Removed property BillingDiscountRedemptionJSON.name

🤖 AI review (confirmed) (85%): The name property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.name as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.object
- object: 'commerce_discount_redemption';

Static analyzer: Removed property BillingDiscountRedemptionJSON.object

🤖 AI review (confirmed) (85%): The object property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.object as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.percent_off
- percent_off?: number;

Static analyzer: Removed property BillingDiscountRedemptionJSON.percent_off

🤖 AI review (confirmed) (85%): The percent_off property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.percent_off as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.promo_code
- promo_code?: string;

Static analyzer: Removed property BillingDiscountRedemptionJSON.promo_code

🤖 AI review (confirmed) (85%): The promo_code property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.promo_code as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.redeemed_at
- redeemed_at: number;

Static analyzer: Removed property BillingDiscountRedemptionJSON.redeemed_at

🤖 AI review (confirmed) (85%): The redeemed_at property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.redeemed_at as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.redeemed_by
- redeemed_by: string | null;

Static analyzer: Removed property BillingDiscountRedemptionJSON.redeemed_by

🤖 AI review (confirmed) (85%): The redeemed_by property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.redeemed_by as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.source
- source: 'promotion' | 'manual' | 'promo_code';

Static analyzer: Removed property BillingDiscountRedemptionJSON.source

🤖 AI review (confirmed) (85%): The source property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.source as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.status
- status?: 'active' | 'exhausted' | 'removed';

Static analyzer: Removed property BillingDiscountRedemptionJSON.status

🤖 AI review (confirmed) (85%): The status property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.status as the entire interface is gone.

Changed: BillingDiscountRedemptionJSON.subscription_item_id
- subscription_item_id: string;

Static analyzer: Removed property BillingDiscountRedemptionJSON.subscription_item_id

🤖 AI review (confirmed) (85%): The subscription_item_id property was removed from BillingDiscountRedemptionJSON, which is itself being removed.

Migration: Remove references to BillingDiscountRedemptionJSON.subscription_item_id as the entire interface is gone.

Changed: BillingDiscounts.discount
- discount?: BillingAppliedDiscount;

Static analyzer: Removed property BillingDiscounts.discount

🤖 AI review (confirmed) (85%): The optional discount property was removed from BillingDiscounts, breaking consumers that read this field.

Migration: Remove any code that accesses .discount on BillingDiscounts objects.

Changed: BillingDiscountsJSON.discount
- discount?: BillingAppliedDiscountJSON;

Static analyzer: Removed property BillingDiscountsJSON.discount

🤖 AI review (confirmed) (85%): The optional discount property was removed from BillingDiscountsJSON, breaking consumers that reference this field.

Migration: Remove any code that accesses .discount on BillingDiscountsJSON objects.

Changed: BillingNamespace.updateCheckout
- updateCheckout: (params: UpdateCheckoutParams) => Promise<BillingCheckoutResource>;

Static analyzer: Removed property BillingNamespace.updateCheckout

🤖 AI review (confirmed) (90%): The updateCheckout method was removed from BillingNamespace, breaking consumers that call this method.

Migration: Remove calls to billing.updateCheckout(...) and find an alternative API for updating checkouts.

Changed: BillingSubscriptionItemJSON.applied_discount
- applied_discount?: BillingDiscountRedemptionJSON;

Static analyzer: Removed property BillingSubscriptionItemJSON.applied_discount

🤖 AI review (confirmed) (85%): The optional applied_discount property was removed from BillingSubscriptionItemJSON, breaking consumers that reference this field.

Migration: Remove any code that accesses .applied_discount on BillingSubscriptionItemJSON objects.

Changed: BillingSubscriptionItemResource.appliedDiscount
- appliedDiscount?: BillingDiscountRedemption;

Static analyzer: Removed property BillingSubscriptionItemResource.appliedDiscount

🤖 AI review (confirmed) (85%): The optional appliedDiscount property was removed from BillingSubscriptionItemResource, breaking consumers that read this field.

Migration: Remove any code that accesses .appliedDiscount on BillingSubscriptionItemResource objects.

Changed: Clerk.setProtectAssertion
- setProtectAssertion: (assertion?: ProtectAssertion) => void;

Static analyzer: Removed property Clerk.setProtectAssertion

🤖 AI review (confirmed) (90%): The setProtectAssertion method was removed from Clerk, breaking consumers that call this method.

Migration: Remove calls to clerk.setProtectAssertion(...) and update any protect assertion logic accordingly.

Changed: ClerkOptions
// ... 3 unchanged lines elided ...
    };
    appearance?: any;
    localization?: LocalizationResource;
-   protectAssertion?: ProtectAssertion;
    polling?: boolean;
    selectInitialSession?: (client: ClientResource) => SignedInSessionResource | null;
    standardBrowser?: boolean;
// ... 27 unchanged lines elided ...

Static analyzer: Breaking change in type alias ClerkOptions: Type changed: import("@clerk/shared").AfterMultiSessionSingleSignOutUrl&import("@clerk/shared").AfterSignOutUrl&import("@clerk/shared…import("@clerk/shared").AfterMultiSessionSingleSignOutUrl&import("@clerk/shared").AfterSignOutUrl&import("@clerk/shared…

🤖 AI review (confirmed) (90%): The protectAssertion optional property was removed from ClerkOptions; consumers who passed this option will no longer have it recognized.

Migration: Remove the protectAssertion field from any ClerkOptions objects you construct.

Changed: HandleOAuthCallbackParams
// ... 15 unchanged lines elided ...
      redirectUrl: string;
      decorateUrl: (url: string) => string;
    }) => Promise<unknown>;
-   __internal_navigate?: (to: string) => Promise<unknown>;
  };

Static analyzer: Breaking change in type alias HandleOAuthCallbackParams: Type changed: import("@clerk/shared").SignInFallbackRedirectUrl&import("@clerk/shared").SignInForceRedirectUrl&import("@clerk/shared"…import("@clerk/shared").SignInFallbackRedirectUrl&import("@clerk/shared").SignInForceRedirectUrl&import("@clerk/shared"…

🤖 AI review (confirmed) (90%): The __internal_navigate optional property was removed from HandleOAuthCallbackParams; consumers who passed this internal field will find it no longer accepted.

Migration: Remove __internal_navigate from any HandleOAuthCallbackParams objects you construct.

Changed: ProtectAssertion
- type ProtectAssertion = string | ProtectAssertionResolver;

Static analyzer: Removed type alias ProtectAssertion

🤖 AI review (confirmed) (90%): The ProtectAssertion type alias was removed entirely, breaking consumers that import or reference this type.

Migration: Remove all imports and references to ProtectAssertion and inline the equivalent type or use an alternative.

Changed: ProtectAssertionResolver
- type ProtectAssertionResolver = () => string | undefined | Promise<string | undefined>;

Static analyzer: Removed type alias ProtectAssertionResolver

🤖 AI review (confirmed) (90%): The ProtectAssertionResolver type alias was removed entirely, breaking consumers that import or reference this type.

Migration: Remove all imports and references to ProtectAssertionResolver and inline the equivalent type or use an alternative.

Changed: UpdateCheckoutParams
- type UpdateCheckoutParams = WithOptionalOrgType<{
-   id: string;
-   promoCode: string;
- }>;

Static analyzer: Removed type alias UpdateCheckoutParams

🤖 AI review (confirmed) (90%): The UpdateCheckoutParams type alias was removed entirely, breaking consumers that import or reference this type.

Migration: Remove all imports and references to UpdateCheckoutParams and update any code that constructs or accepts this type.

🟡 Non-breaking Changes (1)

Modified: __internal_LocalizationResource
Diff (before: 2077 lines, after: 2066 lines). Click to expand.
// ... 105 unchanged lines elided ...
    membershipRole__guestMember: LocalizationValue;
    billing: {
      month: LocalizationValue;
-     months: LocalizationValue;
-     monthAbbreviation: LocalizationValue;
-     monthPerUnit: LocalizationValue<'unitName'>;
-     year: LocalizationValue;
-     years: LocalizationValue;
-     yearAbbreviation: LocalizationValue;
-     yearPerUnit: LocalizationValue<'unitName'>;
-     free: LocalizationValue;
-     getStarted: LocalizationValue;
-     manage: LocalizationValue;
-     manageSubscription: LocalizationValue;
-     cancelSubscription: LocalizationValue;
-     keepSubscription: LocalizationValue;
-     reSubscribe: LocalizationValue;
-     seats: LocalizationValue;
-     seatsWithLimit: LocalizationValue<'limit'>;
-     seatBreakdownSingular: LocalizationValue<'rate'>;
-     seatBreakdownPlural: LocalizationValue<'chargeable' | 'rate'>;
-     seatBreakdownIncludedSingular: LocalizationValue<'totalSeats' | 'included' | 'rate'>;
-     seatBreakdownIncludedPlural: LocalizationValue<'totalSeats' | 'included' | 'chargeable' | 'rate'>;
-     subscribe: LocalizationValue;
-     startFreeTrial: LocalizationValue;
-     startFreeTrial__days: LocalizationValue<'days'>;
-     switchPlan: LocalizationValue;
-     switchToMonthly: LocalizationValue;
-     switchToAnnual: LocalizationValue;
-     switchToMonthlyWithPrice: LocalizationValue<'price'>;
-     switchToAnnualWithAnnualPrice: LocalizationValue<'price'>;
-     billedAnnually: LocalizationValue;
-     billedMonthly: LocalizationValue;
// ... 1098 more lines elided ...
-         planMembershipLimitExceeded: LocalizationValue<'count' | 'limit'>;
-       };
-     };
-     apiKeysPage: {
-       title: LocalizationValue;
-       detailsTitle__emptyRow: LocalizationValue;
-     };
-   };
-   createOrganization: {
-     title: LocalizationValue;
-     formButtonSubmit: LocalizationValue;
-     invitePage: {
-       formButtonReset: LocalizationValue;
-     };
-   };
-   organizationList: {
-     createOrganization: LocalizationValue;
-     title: LocalizationValue<'applicationName'>;
-     titleWithoutPersonal: LocalizationValue;
-     subtitle: LocalizationValue<'applicationName'>;
-     action__invitationAccept: LocalizationValue;
-     invitationAcceptedLabel: LocalizationValue;
-     action__suggestionsAccept: LocalizationValue;
-     suggestionsAcceptedLabel: LocalizationValue;
-     action__createOrganization: LocalizationValue;
-   };
-   oauthConsent: {
-     subtitle: LocalizationValue<'applicationName' | 'identifier'>;
-     scopeList: {
-       privateMetadata: LocalizationValue<'applicationName'>;
+     monthAbbreviation: LocalizationValue;
+     monthPerUnit: LocalizationValue<'unitName'>;
+     year: LocalizationValue;
+     yearAbbreviation: LocalizationValue;
+     yearPerUnit: LocalizationValue<'unitName'>;
+     free: LocalizationValue;
+     getStarted: LocalizationValue;
+     manage: LocalizationValue;
+     manageSubscription: LocalizationValue;
+     cancelSubscription: LocalizationValue;
+     keepSubscription: LocalizationValue;
+     reSubscribe: LocalizationValue;
+     seats: LocalizationValue;
+     seatsWithLimit: LocalizationValue<'limit'>;
+     seatBreakdownSingular: LocalizationValue<'rate'>;
+     seatBreakdownPlural: LocalizationValue<'chargeable' | 'rate'>;
+     seatBreakdownIncludedSingular: LocalizationValue<'totalSeats' | 'included' | 'rate'>;
+     seatBreakdownIncludedPlural: LocalizationValue<'totalSeats' | 'included' | 'chargeable' | 'rate'>;
+     subscribe: LocalizationValue;
+     startFreeTrial: LocalizationValue;
+     startFreeTrial__days: LocalizationValue<'days'>;
+     switchPlan: LocalizationValue;
+     switchToMonthly: LocalizationValue;
+     switchToAnnual: LocalizationValue;
+     switchToMonthlyWithPrice: LocalizationValue<'price'>;
+     switchToAnnualWithAnnualPrice: LocalizationValue<'price'>;
+     billedAnnually: LocalizationValue;
+     billedMonthly: LocalizationValue;
+     billedMonthlyOnly: LocalizationValue;
+     billedAnnuallyOnly: LocalizationValue;
// ... 1087 more lines elided ...
+         noPermissionsToManageBilling: LocalizationValue;
+         planMembershipLimitExceeded: LocalizationValue<'count' | 'limit'>;
+       };
+     };
+     apiKeysPage: {
+       title: LocalizationValue;
+       detailsTitle__emptyRow: LocalizationValue;
+     };
+   };
+   createOrganization: {
+     title: LocalizationValue;
+     formButtonSubmit: LocalizationValue;
+     invitePage: {
+       formButtonReset: LocalizationValue;
+     };
+   };
+   organizationList: {
+     createOrganization: LocalizationValue;
+     title: LocalizationValue<'applicationName'>;
+     titleWithoutPersonal: LocalizationValue;
+     subtitle: LocalizationValue<'applicationName'>;
+     action__invitationAccept: LocalizationValue;
+     invitationAcceptedLabel: LocalizationValue;
+     action__suggestionsAccept: LocalizationValue;
+     suggestionsAcceptedLabel: LocalizationValue;
+     action__createOrganization: LocalizationValue;
+   };
+   oauthConsent: {
+     subtitle: LocalizationValue<'applicationName' | 'identifier'>;
+     scopeList: {
        title: LocalizationValue<'applicationName'>;
      };
      action__deny: LocalizationValue;
// ... 808 unchanged lines elided ...

Static analyzer: Breaking change in type alias __internal_LocalizationResource: Type changed: {locale:string;maintenanceMode:import("@clerk/shared").LocalizationValue;roles:{[r:string]:import("@clerk/shared").Loca…{locale:string;maintenanceMode:import("@clerk/shared").LocalizationValue;roles:{[r:string]:import("@clerk/shared").Loca…

🤖 AI review (reclassified as non-breaking) (55%): The before/after snippets show the same structure with only a line count difference in the elided middle section; __internal_LocalizationResource is used as the base for LocalizationResource which consumers extend via DeepPartial, making any removed keys non-breaking for consumers who only need to supply partial values, but without seeing exactly which keys were removed it's impossible to be certain — the __internal_ prefix strongly suggests this is an internal type not intended for direct consumer use, lowering breaking risk.


@clerk/clerk-js

Version: 6.28.1 → 6.27.1
Recommended bump: MAJOR

Subpath .

🔴 Breaking Changes (1)

Changed: Clerk.setProtectAssertion
- setProtectAssertion: (assertion?: ProtectAssertion) => void;

Static analyzer: Removed property Clerk.setProtectAssertion

🤖 AI review (confirmed) (95%): Removing setProtectAssertion from the Clerk interface means any consumer code calling clerk.setProtectAssertion(...) will fail to compile.

Migration: Remove all calls to clerk.setProtectAssertion(...) in consumer code, as the method no longer exists on the Clerk interface.

Subpath ./no-rhc

🔴 Breaking Changes (1)

Changed: Clerk.setProtectAssertion
- setProtectAssertion: (assertion?: ProtectAssertion) => void;

Static analyzer: Removed property Clerk.setProtectAssertion

🤖 AI review (confirmed) (95%): Removing setProtectAssertion from the Clerk interface breaks any consumer code that calls clerk.setProtectAssertion(...).

Migration: Remove all calls to clerk.setProtectAssertion() from consumer code, as the method no longer exists on the Clerk interface.


@clerk/ui

Version: 1.30.1 → 1.29.1
Recommended bump: MINOR

Subpath ./internal

🟡 Non-breaking Changes (1)

Modified: ElementsConfig
// ... 70 unchanged lines elided ...
    alternativeMethodsBlockButtonText: WithOptions<OAuthProvider | Web3Provider | PhoneCodeChannel>;
    alternativeMethodsBlockButtonArrow: WithOptions<OAuthProvider | Web3Provider | PhoneCodeChannel>;
    checkoutFormLineItemsRoot: WithOptions;
-   checkoutFormPromoCodeRoot: WithOptions;
-   checkoutFormPromoCodeInput: WithOptions;
-   checkoutFormPromoCodeApplyButton: WithOptions;
-   checkoutFormPromoCodeErrorText: WithOptions;
-   checkoutFormPromoCodeRemoveButton: WithOptions;
    checkoutFormElementsRoot: WithOptions;
    checkoutSuccessRoot: WithOptions;
    checkoutSuccessRings: WithOptions;
// ... 481 unchanged lines elided ...

Static analyzer: Breaking change in type alias ElementsConfig: Type changed: {button:import("@clerk/ui").~WithOptions<string>;input:import("@clerk/ui").~WithOptions;checkbox:import("@clerk/ui").~W…{button:import("@clerk/ui").~WithOptions<string>;input:import("@clerk/ui").~WithOptions;checkbox:import("@clerk/ui").~W…

🤖 AI review (reclassified as non-breaking) (82%): The diff shows 482 elided lines shrinking to 477, indicating that 5 keys were removed from ElementsConfig; however, ElementsConfig is used only as a mapped-type key source for the output type Elements (consumers only read Elements values, never construct an ElementsConfig object), and the removal of keys from an output-only type alias narrows the union, which is safe for consumers. The before/after snippets show identical visible keys at top and bottom, and the elision count difference is consistent with a small removal of internal keys that consumers do not construct or pass as inputs.


@clerk/vue

Version: 2.4.27 → 2.4.25
Recommended bump: MINOR

🟡 Non-breaking Changes (1)

Modified: AuthenticateWithRedirectCallback

// ... 15 unchanged lines elided ...
          redirectUrl: string;
          decorateUrl: (url: string) => string;
      }) => Promise<unknown>;
-     __internal_navigate?: (to: string) => Promise<unknown>;
  } & {}, import("vue").PublicProps>

Static analyzer: Breaking change in variable AuthenticateWithRedirectCallback: Type changed: import("@vue/runtime-core").DefineSetupFnComponent<import("@clerk/shared").HandleOAuthCallbackParams,{},{},import("@cle…import("@vue/runtime-core").DefineSetupFnComponent<import("@clerk/shared").HandleOAuthCallbackParams,{},{},import("@cle…

🤖 AI review (reclassified as non-breaking) (85%): The only difference between before and after is the removal of the __internal_navigate property (prefixed with __internal_, indicating it is an internal implementation detail). Since this is a prop type for a Vue component (input position), removing a property from the prop type widens the accepted set for consumers who use the component (they no longer need to pass it), and the __internal_ prefix signals it was never intended for public consumer use. No well-typed consumer should have been relying on this internal prop.


Report generated by Break Check

Last ran on af5f90d.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: e06bfa40-bf96-4071-89f5-c32a12b34f03

📥 Commits

Reviewing files that changed from the base of the PR and between 89be03b and af5f90d.

📒 Files selected for processing (37)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/dialog/use-dialog-origin.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.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/cli (auto-detected)
  • clerk/clerk-ios (auto-detected)
  • clerk/clerk-android (auto-detected)
💤 Files with no reviewable changes (2)
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/components/dialog.tsx

📝 Walkthrough

Walkthrough

The PR replaces the Mosaic Dialog implementation with a headless-backed compound API. It adds detached triggers, typed payloads, imperative handles, trigger metadata, dismissal modes, configurable focus targets, nested-dialog state, StyleX styling, keyboard insets, browser-chrome synchronization, and Dialog.CloseButton. It updates exports, tests, documentation, stories, changesets, and selected dialog integrations. Button also gains the xstyle prop.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: austincalvelage, alexcarpenter

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
Title check ✅ Passed The title clearly summarizes the main StyleX Dialog rebuild and composition API changes.
Description check ✅ Passed The description directly explains the Dialog rebuild, styling changes, composition APIs, and related follow-up work.
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.

@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.

Actionable comments posted: 7

🧹 Nitpick comments (10)
packages/ui/src/mosaic/components/dialog/keyboard-inset.ts (1)

75-82: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the release function idempotent.

Each call to the returned function decrements listeners. A second call on the same release drives the count below zero. The counter then never returns to 0, and the listeners plus the --_cl-keyboard-inset property stay attached for the lifetime of the page. acquireBrowserChrome in browser-chrome.ts guards this case with a released flag; this module does not.

♻️ Proposed guard
-  return () => {
-    listeners--;
-    if (listeners === 0 && detach) {
-      detach();
-      detach = null;
-    }
-  };
+  let released = false;
+  return () => {
+    if (released) {
+      return;
+    }
+    released = true;
+    listeners--;
+    if (listeners === 0 && detach) {
+      detach();
+      detach = null;
+    }
+  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/keyboard-inset.ts` around lines 75 -
82, Make the release function returned by the keyboard-inset acquisition flow
idempotent by adding a per-release guard, similar to acquireBrowserChrome’s
released flag. Only decrement listeners and detach the keyboard-inset
listener/property when that release has not already been executed.
packages/ui/src/mosaic/components/dialog/dialog.tsx (1)

205-229: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

render in rest silently replaces the styled Button.

CloseButton sets render before {...rest}. If a consumer passes render, their element replaces the Button wrapper, and styles.closeButton plus closeInsets[size] are lost. The button then loses its absolute anchoring. Consider omitting render from DialogCloseButtonProps, or documenting that the override must supply its own positioning.

♻️ Proposed type change
-export interface DialogCloseButtonProps extends MosaicComponentProps<'button'> {
+export interface DialogCloseButtonProps extends Omit<MosaicComponentProps<'button'>, 'render'> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/dialog.tsx` around lines 205 - 229,
Prevent consumers from overriding the internal render used by CloseButton: omit
render from DialogCloseButtonProps and exclude it from the rest props spread,
preserving the styled Button with styles.closeButton and closeInsets[size].
packages/ui/src/mosaic/components/dialog/dialog.styles.ts (1)

199-232: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Trim the duplicated and oversized comments in sizes.panel.

Two blocks state the same fact. Lines 214-223 explain that the panel fills the viewport content box with stretch, and lines 224-228 repeat it. Reduce the block to a single terse note. The same applies across this file, where multi-paragraph rationale blocks dominate the style declarations.

The coding guidelines require minimal comments: "Add comments only when critical to explain why a non-obvious change was made; never restate code behavior, and keep warranted comments to one terse line rather than a verbose multi-line block."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/dialog.styles.ts` around lines 199 -
232, Trim the duplicated rationale comments in sizes.panel, especially the
repeated explanation around alignSelf: 'stretch', leaving one concise line only
where the non-obvious layout decision requires justification. Apply the same
minimal-comment standard to nearby oversized rationale blocks in this file
without changing the style declarations.

Source: Coding guidelines

packages/ui/src/mosaic/components/dialog/browser-chrome.ts (2)

160-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The comment describes a binary search; the code performs a linear scan.

makeEasing walks the table with while (lo < SAMPLES && table[lo + 1] < x) lo++. That is a linear scan, not a binary search. Correct the comment or implement the search that it describes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/browser-chrome.ts` around lines 160
- 190, Update makeEasing so its lookup uses a binary search over the monotonic
table rather than incrementing lo through entries linearly; preserve the
existing interpolation and axis calculation behavior.

229-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The acquireBrowserChrome doc block is attached to resolveTint.

Lines 229-237 document acquireBrowserChrome and its @param backdrop. A second doc block for resolveTint follows at lines 238-243, and the function resolveTint declaration follows that. TypeScript and editors therefore associate the first block with nothing, and acquireBrowserChrome at line 259 has no JSDoc. Move the first block directly above export function acquireBrowserChrome.

The coding guidelines require that "All public APIs must be documented with JSDoc".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/browser-chrome.ts` around lines 229
- 257, Move the first JSDoc block describing the refcounted backdrop behavior
and its backdrop parameter from above resolveTint to directly above export
function acquireBrowserChrome. Keep the separate resolveTint documentation
attached to resolveTint, ensuring the public acquireBrowserChrome API retains
its documentation.

Source: Coding guidelines

packages/ui/src/mosaic/styles/index.ts (1)

17-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Re-export the headless dialog types that the exported prop types reference.

packages/ui/src/mosaic/components/dialog/index.ts also exports DialogFocusTarget, DialogHandle, and DialogOpenChangeDetails. This barrel omits them. DialogRootProps and DialogTriggerProps carry a handle?: DialogHandle<Payload> member, and DialogPopupProps carries initialFocus/finalFocus of type DialogFocusTarget. A consumer of this entry point can therefore pass those props but cannot name their types.

♻️ Proposed addition
 export { Dialog } from '../components/dialog';
 export type {
   DialogBackdropProps,
   DialogCloseButtonProps,
   DialogCloseProps,
   DialogDescriptionProps,
+  DialogFocusTarget,
+  DialogHandle,
+  DialogOpenChangeDetails,
   DialogPopupProps,
   DialogProps,
   DialogRootProps,
   DialogSize,
   DialogTitleProps,
   DialogTriggerProps,
   DialogViewportProps,
 } from '../components/dialog';
🤖 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/styles/index.ts` around lines 17 - 30, Update the
dialog type re-exports in the styles barrel to include DialogFocusTarget,
DialogHandle, and DialogOpenChangeDetails from the existing dialog component
exports, alongside the current DialogRootProps, DialogTriggerProps, and
DialogPopupProps types.
packages/headless/src/primitives/dialog/dialog.test.tsx (1)

385-455: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for a Dialog.Trigger with neither a root nor a handle.

dialog-trigger.tsx line 36 throws a documented error for this case. No test covers it. The test also exposes the hook-order problem flagged in packages/headless/src/primitives/dialog/dialog-trigger.tsx lines 33-37, because React reports a hook-count error instead of the intended message once a store disappears between renders.

💚 Proposed test
+    it('throws when the trigger has neither a root nor a handle', () => {
+      const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
+      expect(() => render(<Dialog.Trigger>Orphan</Dialog.Trigger>)).toThrow(
+        /must be nested in a <Dialog.Root> or given a `handle`/,
+      );
+      consoleError.mockRestore();
+    });
🤖 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/headless/src/primitives/dialog/dialog.test.tsx` around lines 385 -
455, Add a test in the detached-trigger describe block that renders
Dialog.Trigger without a Dialog.Root or handle and asserts the documented error
from Dialog.Trigger. Include a rerender or unmount scenario where the associated
store disappears to verify stable hook ordering and ensure the intended error is
reported instead of a React hook-count error.

Source: Coding guidelines

packages/headless/src/primitives/dialog/dialog-root.tsx (1)

121-143: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Confirm the stability assumptions in the setRoot effect.

The effect only re-runs when store changes. openFromTrigger and closeFromTrigger capture refs, floatingContext, and setActiveTriggerId from the render that attached the controller. setActiveTriggerId comes from useControllableState, whose setter identity depends on isControlled. If a consumer switches triggerId between undefined and a value after mount, the captured setter becomes stale and trigger attribution stops updating. applyOpenChange already avoids this through the latest ref; consider routing setActiveTriggerId and setActivePayload through the same ref.

♻️ Proposed change to route trigger state through the latest ref
-  const latest = useRef({ applyOpenChange, activeTriggerId });
+  const latest = useRef({ applyOpenChange, activeTriggerId, setActiveTriggerId, setActivePayload });
   useLayoutEffect(() => {
-    latest.current = { applyOpenChange, activeTriggerId };
+    latest.current = { applyOpenChange, activeTriggerId, setActiveTriggerId, setActivePayload };
   });
 
   useLayoutEffect(() => {
     return store.setRoot({
       openFromTrigger: (id, event) => {
         const registration = store.getTrigger(id);
-        setActiveTriggerId(id);
-        setActivePayload(registration?.payload);
+        latest.current.setActiveTriggerId(id);
+        latest.current.setActivePayload(registration?.payload);
🤖 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/headless/src/primitives/dialog/dialog-root.tsx` around lines 121 -
143, Update the setRoot effect callbacks in the dialog root to access
setActiveTriggerId and setActivePayload through the latest ref, matching the
existing latest.current.applyOpenChange pattern. Ensure openFromTrigger always
uses the current controllable-state setters when triggerId control changes,
while preserving the existing trigger registration, reference assignment,
pending details, and open-change behavior.
packages/headless/src/primitives/dialog/dialog-handle.ts (1)

59-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused getRegistryVersion member.

dialog-root.tsx re-resolves through store.subscribe, and no caller reads getRegistryVersion. Remove the member, counter, and getter.

🤖 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/headless/src/primitives/dialog/dialog-handle.ts` around lines 59 -
60, Remove the unused getRegistryVersion() member from the dialog handle
contract, along with the registry version counter and its getter implementation.
Preserve the existing store.subscribe-based re-resolution in dialog-root.tsx and
remove only the obsolete registry-version plumbing.
packages/headless/src/primitives/dialog/use-dialog-origin.ts (1)

34-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Condense the two explanatory comment blocks.

The coding guidelines state: "keep warranted comments to one terse line rather than a verbose multi-line block". The measurement reasoning is worth recording, but six-line and four-line blocks exceed that. Reduce each to one line, or move the full rationale into the function JSDoc at Lines 8-19.

Also applies to: 50-53

🤖 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/headless/src/primitives/dialog/use-dialog-origin.ts` around lines 34
- 39, Condense the explanatory comment blocks surrounding the measurement logic
in use-dialog-origin, including the blocks near lines 34-39 and 50-53, to one
terse line each. Preserve the essential rationale about scaled
getBoundingClientRect values, unscaled offset dimensions, and transform-origin
coordinates, without changing the implementation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/olive-doors-tell.md:
- Around line 2-3: Update the Dialog changes in the relevant UI and headless
compatibility implementations to retain deprecated support for the removed sx
and existing size APIs, preserving consumers on `@clerk/ui`@1 during this patch
release. If compatibility cannot be preserved, change the changeset entries for
`@clerk/headless` and `@clerk/ui` from patch to major and document the migration
path.

In `@packages/headless/src/primitives/dialog/dialog-root.tsx`:
- Around line 117-178: Add an SSR-safe layout-effect helper in the dialog
primitives and use it for every useLayoutEffect shown in DialogInner, including
the latest ref, root registration, reference resolution, payload lookup, state
publication, and cleanup effects. Preserve each effect’s dependencies, cleanup
behavior, and execution order while replacing the direct layout-effect usage
with the helper.

In `@packages/headless/src/primitives/dialog/README.md`:
- Around line 151-171: Qualify the dialog dismissal documentation to reflect
that Escape and outside-press dismissal depend on closedBy: in
packages/headless/src/primitives/dialog/README.md lines 151-171, state that
Escape closes only when closedBy is not 'none'; in
packages/swingset/src/stories/dialog.component.mdx lines 54-55, replace the
unconditional “always” wording with behavior conditional on the default
closedBy='any' value.
- Around line 105-108: Update the initialFocus and finalFocus callback
documentation for Dialog.Popup to remove refs from the callback return options.
Document callback results as boolean, void, HTMLElement, or null, while keeping
refs listed only as supported direct values.

In `@packages/headless/src/primitives/dialog/use-dialog-origin.ts`:
- Around line 25-54: Reset the popup’s ORIGIN_PROPERTY to the neutral center
value before calling getBoundingClientRect() in the useLayoutEffect, ensuring
reused popups are measured without the previous transform origin. Keep the
existing open/popup/trigger guards and origin calculation unchanged, then set
the computed origin afterward.

In `@packages/swingset/src/stories/dialog.component.mdx`:
- Around line 275-286: Add the missing stylex import to the panel example before
its usage in the Dialog content, ensuring the existing stylex.props calls
resolve when the snippet is copied.

In `@packages/ui/src/mosaic/components/dialog/dialog.test.tsx`:
- Around line 541-557: Complete the test around the stacked-dialog teardown flow
by sending a second Escape after the existing inner-dialog assertion, then use
waitFor to assert that themeColor() becomes null after the outer dialog closes
and deferred fade cleanup finishes.

---

Nitpick comments:
In `@packages/headless/src/primitives/dialog/dialog-handle.ts`:
- Around line 59-60: Remove the unused getRegistryVersion() member from the
dialog handle contract, along with the registry version counter and its getter
implementation. Preserve the existing store.subscribe-based re-resolution in
dialog-root.tsx and remove only the obsolete registry-version plumbing.

In `@packages/headless/src/primitives/dialog/dialog-root.tsx`:
- Around line 121-143: Update the setRoot effect callbacks in the dialog root to
access setActiveTriggerId and setActivePayload through the latest ref, matching
the existing latest.current.applyOpenChange pattern. Ensure openFromTrigger
always uses the current controllable-state setters when triggerId control
changes, while preserving the existing trigger registration, reference
assignment, pending details, and open-change behavior.

In `@packages/headless/src/primitives/dialog/dialog.test.tsx`:
- Around line 385-455: Add a test in the detached-trigger describe block that
renders Dialog.Trigger without a Dialog.Root or handle and asserts the
documented error from Dialog.Trigger. Include a rerender or unmount scenario
where the associated store disappears to verify stable hook ordering and ensure
the intended error is reported instead of a React hook-count error.

In `@packages/headless/src/primitives/dialog/use-dialog-origin.ts`:
- Around line 34-39: Condense the explanatory comment blocks surrounding the
measurement logic in use-dialog-origin, including the blocks near lines 34-39
and 50-53, to one terse line each. Preserve the essential rationale about scaled
getBoundingClientRect values, unscaled offset dimensions, and transform-origin
coordinates, without changing the implementation.

In `@packages/ui/src/mosaic/components/dialog/browser-chrome.ts`:
- Around line 160-190: Update makeEasing so its lookup uses a binary search over
the monotonic table rather than incrementing lo through entries linearly;
preserve the existing interpolation and axis calculation behavior.
- Around line 229-257: Move the first JSDoc block describing the refcounted
backdrop behavior and its backdrop parameter from above resolveTint to directly
above export function acquireBrowserChrome. Keep the separate resolveTint
documentation attached to resolveTint, ensuring the public acquireBrowserChrome
API retains its documentation.

In `@packages/ui/src/mosaic/components/dialog/dialog.styles.ts`:
- Around line 199-232: Trim the duplicated rationale comments in sizes.panel,
especially the repeated explanation around alignSelf: 'stretch', leaving one
concise line only where the non-obvious layout decision requires justification.
Apply the same minimal-comment standard to nearby oversized rationale blocks in
this file without changing the style declarations.

In `@packages/ui/src/mosaic/components/dialog/dialog.tsx`:
- Around line 205-229: Prevent consumers from overriding the internal render
used by CloseButton: omit render from DialogCloseButtonProps and exclude it from
the rest props spread, preserving the styled Button with styles.closeButton and
closeInsets[size].

In `@packages/ui/src/mosaic/components/dialog/keyboard-inset.ts`:
- Around line 75-82: Make the release function returned by the keyboard-inset
acquisition flow idempotent by adding a per-release guard, similar to
acquireBrowserChrome’s released flag. Only decrement listeners and detach the
keyboard-inset listener/property when that release has not already been
executed.

In `@packages/ui/src/mosaic/styles/index.ts`:
- Around line 17-30: Update the dialog type re-exports in the styles barrel to
include DialogFocusTarget, DialogHandle, and DialogOpenChangeDetails from the
existing dialog component exports, alongside the current DialogRootProps,
DialogTriggerProps, and DialogPopupProps types.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: e06bfa40-bf96-4071-89f5-c32a12b34f03

📥 Commits

Reviewing files that changed from the base of the PR and between 89be03b and af5f90d.

📒 Files selected for processing (37)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/dialog/use-dialog-origin.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.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/cli (auto-detected)
  • clerk/clerk-ios (auto-detected)
  • clerk/clerk-android (auto-detected)
💤 Files with no reviewable changes (2)
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/components/dialog.tsx

Comment on lines +2 to +3
'@clerk/headless': patch
'@clerk/ui': patch

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve the removed Dialog APIs in this patch release.

This changeset states that consumers must replace sx and migrate existing size usage. That breaks consumers that remain on @clerk/ui@1 after a patch upgrade.

Keep deprecated compatibility paths for these APIs in a non-major release. Otherwise, publish this as a major release with a migration path.

As per coding guidelines, “Maintain backward compatibility in packages/clerk-js and packages/ui with SDK versions already in the wild.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/olive-doors-tell.md around lines 2 - 3, Update the Dialog changes
in the relevant UI and headless compatibility implementations to retain
deprecated support for the removed sx and existing size APIs, preserving
consumers on `@clerk/ui`@1 during this patch release. If compatibility cannot be
preserved, change the changeset entries for `@clerk/headless` and `@clerk/ui` from
patch to major and document the migration path.

Sources: Coding guidelines, Linked repositories

Comment on lines +117 to +178
useLayoutEffect(() => {
latest.current = { applyOpenChange, activeTriggerId };
});

useLayoutEffect(() => {
return store.setRoot({
openFromTrigger: (id, event) => {
const registration = store.getTrigger(id);
setActiveTriggerId(id);
setActivePayload(registration?.payload);
if (registration) {
refs.setReference(registration.element);
}
pendingDetailsRef.current = { trigger: registration?.element ?? null, triggerId: id, event };
floatingContext.onOpenChange(true, event, 'click');
},
closeFromTrigger: (id, event) => {
const registration = store.getTrigger(id);
pendingDetailsRef.current = { trigger: registration?.element ?? null, triggerId: id, event };
floatingContext.onOpenChange(false, event, 'click');
},
setOpen: nextOpen => {
latest.current.applyOpenChange(nextOpen, { trigger: null, triggerId: null, event: undefined });
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps -- floatingContext.onOpenChange, setActiveTriggerId and refs are stable
}, [store]);

// The floating reference is the ACTIVE trigger — origin measurement, return focus, and
// outside-press exclusion all read `elements.domReference`. With no active trigger the first
// registered one stands in, preserving single-trigger behaviour for `defaultOpen` dialogs.
//
// Subscribed imperatively rather than through `useSyncExternalStore`: re-registration must not
// re-render this component, or a trigger whose `payload` is an inline object literal would
// re-register on every render of its own and the two would feed each other forever.
useLayoutEffect(() => {
const resolve = () => {
const active = activeTriggerId != null ? store.getTrigger(activeTriggerId) : undefined;
refs.setReference(active?.element ?? store.getFirstTrigger()?.element ?? null);
};
resolve();
return store.subscribe(resolve);
// eslint-disable-next-line react-hooks/exhaustive-deps -- refs is stable
}, [store, activeTriggerId]);

// For opens that arrive without a trigger activation — a controlled `open`/`triggerId` pair,
// `defaultOpen` — the payload is looked up from the registry once the dialog is open. Runs
// after the children's layout effects, so triggers rendered inside the root are registered by
// the time it reads, and the pre-paint re-render delivers their payload on the first frame.
useLayoutEffect(() => {
if (open) {
setActivePayload(activeTriggerId != null ? store.getTrigger(activeTriggerId)?.payload : undefined);
}
}, [store, open, activeTriggerId]);

// What detached triggers render their open state and ARIA wiring from.
useLayoutEffect(() => {
store.publishState({ open, triggerId: activeTriggerId, popupId: floatingContext.floatingId });
}, [store, open, activeTriggerId, floatingContext.floatingId]);
useLayoutEffect(() => {
return () => store.publishState({ open: false, triggerId: null, popupId: undefined });
}, [store]);

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for an existing isomorphic layout effect helper and how other primitives handle SSR.
rg -nP -g 'packages/headless/**/*.{ts,tsx}' -C2 'useIsomorphicLayoutEffect|useIsoLayoutEffect|typeof (window|document) !== .undefined.'
# Check how many primitives call useLayoutEffect directly.
rg -nc -g 'packages/headless/src/**/*.tsx' 'useLayoutEffect'

Repository: clerk/javascript

Length of output: 154


🏁 Script executed:

#!/bin/bash
set +e
printf '%s\n' '--- candidate files ---'
git ls-files 'packages/headless/**' | sed -n '1,160p'
printf '%s\n' '--- reviewed file imports and effects ---'
sed -n '1,230p' packages/headless/src/primitives/dialog/dialog-root.tsx
printf '%s\n' '--- all effect/helper references in headless ---'
rg -n -S -g 'packages/headless/**/*.{ts,tsx,js,jsx}' \
  'use(Layout|Effect)|use(Isomorphic|Iso).*Effect|isomorphic|typeof[[:space:]]*\((window|document)\)|typeof[[:space:]]+(window|document)' \
  || true
printf '%s\n' '--- package metadata and SSR references ---'
rg -n -S -g 'packages/headless/**' -g 'packages/*/package.json' \
  'SSR|server rendering|server-side|useLayoutEffect|react-dom/server|renderToString|renderToPipeableStream|react-server' \
  || true

Repository: clerk/javascript

Length of output: 18699


🏁 Script executed:

#!/bin/bash
set +e
printf '%s\n' '--- direct layout-effect usage ---'
rg -n -S 'useLayoutEffect|useIsomorphicLayoutEffect|useIsoLayoutEffect' packages/headless/src || true
printf '%s\n' '--- SSR test ---'
sed -n '1,240p' packages/headless/src/primitives/drawer/drawer.ssr.test.tsx
printf '%s\n' '--- package metadata ---'
cat packages/headless/package.json
printf '%s\n' '--- package README SSR references ---'
rg -n -i -C3 'ssr|server|render' packages/headless/README.md packages/headless/src packages/headless/package.json || true
printf '%s\n' '--- repository-wide effect helper definitions ---'
rg -n -S 'use(Isomorphic|Iso).*Effect|isomorphic.*effect|useLayoutEffect' packages | sed -n '1,240p'

Repository: clerk/javascript

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set +e
printf '%s\n' '--- exact layout-effect references in source ---'
rg -n -S '\buseLayoutEffect\b|\buseIsomorphicLayoutEffect\b|\buseIsoLayoutEffect\b' packages/headless/src --glob '!**/*.test.*' || true
printf '%s\n' '--- exact layout-effect references in tests ---'
rg -n -S '\buseLayoutEffect\b|\buseIsomorphicLayoutEffect\b|\buseIsoLayoutEffect\b' packages/headless/src --glob '**/*.test.*' || true
printf '%s\n' '--- drawer SSR test ---'
cat -n packages/headless/src/primitives/drawer/drawer.ssr.test.tsx
printf '%s\n' '--- package metadata ---'
cat packages/headless/package.json
printf '%s\n' '--- SSR-related package files ---'
rg -n -i -S 'SSR|server rendering|server-side|react-dom/server|renderToString|renderToPipeableStream' packages/headless --glob '!**/*.test.*' || true

Repository: clerk/javascript

Length of output: 8072


🌐 Web query:

React useLayoutEffect server rendering warning official documentation

💡 Result:

When using useLayoutEffect in a server-side rendering (SSR) environment, React issues a warning because the hook is designed to perform DOM operations or measurements that rely on layout information which does not exist on the server [1][2][3]. Since useLayoutEffect only runs on the client, it cannot affect the initial HTML generated during server rendering, potentially leading to a "flash" or a mismatch between the server-rendered UI and the intended client-rendered UI [3][4][5]. According to official React documentation and guidance, there are several recommended approaches to resolve this warning [1][2]: 1. Replace with useEffect: If the logic does not strictly need to block the browser from repainting the screen (e.g., it is not measuring layout for immediate positioning), replace useLayoutEffect with useEffect [1][2]. This will suppress the warning because useEffect is intended to run after the paint [1][3]. 2. Defer rendering until client-side hydration: If the component's UI depends on layout measurements and would appear broken without them, delay its rendering until after the component has mounted on the client [1][2]. You can achieve this by using a state variable (e.g., isMounted) initialized to false, and setting it to true within a useEffect hook [1][2][6]. The component will then only render its layout-dependent content after hydration is complete [1]. 3. Use useSyncExternalStore: If you are synchronizing your component with an external data store rather than performing DOM measurements, consider using the useSyncExternalStore hook, which is explicitly designed to support server rendering [1][2]. Attempting to create a custom "isomorphic" layout effect hook by simply suppressing the warning does not make the hook work on the server; it only hides the diagnostic message while the underlying issue—that the effect cannot execute on the server—remains [5].

Citations:


🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
from pathlib import Path
import re

root = Path("packages/headless/src")
source_files = [
    p for p in root.rglob("*")
    if p.suffix in {".ts", ".tsx"} and ".test." not in p.name
]
direct = []
for path in source_files:
    text = path.read_text()
    count = len(re.findall(r"\buseLayoutEffect\b", text))
    if count:
        direct.append((str(path), count))

print("direct_useLayoutEffect_files=", len(direct))
print("direct_useLayoutEffect_occurrences=", sum(count for _, count in direct))
for path, count in direct:
    print(f"{path}: {count}")
print("isomorphic_helper_names_found=", any(
    re.search(r"\buse(?:Isomorphic|Iso)LayoutEffect\b", p.read_text())
    for p in source_files
))
print("ssr_test_files=", [
    str(p) for p in root.rglob("*.ssr.test.*")
])
PY

Repository: clerk/javascript

Length of output: 895


Add an SSR-safe layout-effect helper for DialogInner

@clerk/headless supports SSR but has no such helper. Add one and use it for these effects to prevent React’s server-rendering warning.

🤖 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/headless/src/primitives/dialog/dialog-root.tsx` around lines 117 -
178, Add an SSR-safe layout-effect helper in the dialog primitives and use it
for every useLayoutEffect shown in DialogInner, including the latest ref, root
registration, reference resolution, payload lookup, state publication, and
cleanup effects. Preserve each effect’s dependencies, cleanup behavior, and
execution order while replacing the direct layout-effect usage with the helper.

Source: Coding guidelines

Comment on lines +105 to +108
`initialFocus` and `finalFocus` on `Dialog.Popup` control where focus moves on open and close.
Each accepts `true` (the default behaviour), `false` (do not move focus), a ref, or a function of
the interaction type behind the open/close (`'mouse' | 'touch' | 'pen' | 'keyboard' | ''`, empty
for programmatic) returning any of those:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline packages/headless/src/primitives/dialog --items all --match 'DialogFocusTarget|initialFocus|finalFocus'
rg -n -C 4 '\b(DialogFocusTarget|initialFocus|finalFocus)\b' packages/headless/src/primitives/dialog -g '*.ts' -g '*.tsx'

Repository: clerk/javascript

Length of output: 22452


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- README focus documentation ---'
cat -n packages/headless/src/primitives/dialog/README.md | sed -n '95,115p;190,210p'

printf '%s\n' '--- exported focus type and implementation ---'
cat -n packages/headless/src/primitives/dialog/dialog-popup.tsx | sed -n '10,32p;60,86p;86,116p'
cat -n packages/headless/src/primitives/dialog/index.ts | sed -n '1,25p'

printf '%s\n' '--- focus-related README references ---'
rg -n -C 3 'RefObject|callback|function|initialFocus|finalFocus|focus target' packages/headless/src/primitives/dialog/README.md

Repository: clerk/javascript

Length of output: 10340


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

source = Path("packages/headless/src/primitives/dialog/dialog-popup.tsx").read_text()
readme = Path("packages/headless/src/primitives/dialog/README.md").read_text()

type_match = re.search(
    r"export type DialogFocusTarget\s*=\s*(.*?);",
    source,
    re.S,
)
assert type_match, "DialogFocusTarget declaration not found"
type_text = " ".join(type_match.group(1).split())

callback_match = re.search(r"\(\(interactionType: InteractionType\) => ([^)]+)\)", type_text)
assert callback_match, "DialogFocusTarget callback return type not found"
callback_return = callback_match.group(1)

claim_match = re.search(
    r"Each accepts .*?a ref, or a function .*?returning any of those:",
    readme,
    re.S,
)
assert claim_match, "README focus-target claim not found"

print("DialogFocusTarget:", type_text)
print("Callback return type:", callback_return)
print("Callback return includes RefObject:", "RefObject" in callback_return)
print("README says callback returns the ref option:", "returning any of those" in claim_match.group(0))
PY

Repository: clerk/javascript

Length of output: 449


Correct the focus callback documentation.

DialogFocusTarget allows refs only as direct values. Callback results allow boolean, void, HTMLElement, or null. Remove refs from the callback description.

🤖 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/headless/src/primitives/dialog/README.md` around lines 105 - 108,
Update the initialFocus and finalFocus callback documentation for Dialog.Popup
to remove refs from the callback return options. Document callback results as
boolean, void, HTMLElement, or null, while keeping refs listed only as supported
direct values.

Source: Coding guidelines

Comment on lines +25 to +54
useLayoutEffect(() => {
const popup = popupRef.current;
if (!open || !popup || !trigger) {
return;
}

const triggerRect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();

// `getBoundingClientRect` reports the SCALED box, and the entering frame is already at
// `scale(0.98)`. Its CENTRE is not affected, though — the property is still unset at this
// point, so that scale is about `center` — and `offsetWidth`/`offsetHeight` are the
// unscaled layout dimensions. Together they recover the untransformed box, which is what
// `transform-origin`'s coordinates are relative to. Measuring the scaled edges instead
// would offset the origin by half the scale delta on each axis.
const centerX = popupRect.left + popupRect.width / 2;
const centerY = popupRect.top + popupRect.height / 2;
const layoutLeft = centerX - popup.offsetWidth / 2;
const layoutTop = centerY - popup.offsetHeight / 2;

const originX = triggerRect.left + triggerRect.width / 2 - layoutLeft;
const originY = triggerRect.top + triggerRect.height / 2 - layoutTop;

popup.style.setProperty(ORIGIN_PROPERTY, `${originX}px ${originY}px`);

// Runs in a layout effect, so this lands before paint on the frame that still carries
// `data-starting-style` — the frame pinned at `opacity: 0` with `transition: none`. Moving
// the origin repositions the scaled box, and that reflow is invisible for the same reason
// the popover's is: nothing is painted yet, and the transition arms a frame later.
}, [popupRef, trigger, open]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Determine whether the dialog popup element can survive a close/reopen cycle.
set -euo pipefail

fd . packages/headless/src/primitives/dialog --type f --extension ts --extension tsx --exec cat -n {}

# Look for keepMounted / persistence options that would retain the popup node.
rg -n -C4 'keepMounted|unmount|mounted' packages/headless/src/primitives/dialog packages/headless/src/utils

Repository: clerk/javascript

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- use-dialog-origin.ts ---'
cat -n packages/headless/src/primitives/dialog/use-dialog-origin.ts

printf '%s\n' '--- use-transition implementation and usages ---'
rg -n -C5 'export function useTransition|function useTransition|useTransition\(' packages/headless/src/hooks packages/headless/src/primitives/dialog

printf '%s\n' '--- persistence-related dialog APIs ---'
rg -n -C3 'keepMounted|unmount|mounted|presence|exit|transition' packages/headless/src packages/headless/test packages/headless 2>/dev/null | head -240

Repository: clerk/javascript

Length of output: 34603


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- use-transition-status.ts ---'
cat -n packages/headless/src/hooks/use-transition-status.ts

printf '%s\n' '--- use-transition.ts ---'
cat -n packages/headless/src/hooks/use-transition.ts

printf '%s\n' '--- animation-finish helper ---'
cat -n packages/headless/src/hooks/use-animations-finished.ts

printf '%s\n' '--- rapid close/open transition tests ---'
sed -n '145,220p' packages/headless/src/hooks/use-transition.test.ts

Repository: clerk/javascript

Length of output: 10946


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C5 -- '--cl-dialog-origin|data-starting-style|data-ending-style' packages | head -300

Repository: clerk/javascript

Length of output: 29599


Reset --cl-dialog-origin before measuring a reused popup.

useTransition keeps the popup mounted during an exit animation and supports rapid close-to-open cancellation. The next layout effect can measure the same element with the previous --cl-dialog-origin. Clear the property or set it to center before reading getBoundingClientRect().

🤖 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/headless/src/primitives/dialog/use-dialog-origin.ts` around lines 25
- 54, Reset the popup’s ORIGIN_PROPERTY to the neutral center value before
calling getBoundingClientRect() in the useLayoutEffect, ensuring reused popups
are measured without the previous transform origin. Keep the existing
open/popup/trigger guards and origin calculation unchanged, then set the
computed origin afterward.

Source: Coding guidelines

Comment on lines +275 to +286
```tsx
import { scrollAreaRoot, scrollAreaViewport } from '@clerk/ui/mosaic/components/scroll-area';

<Dialog size='panel' trigger={props => <Button {...props}>Open settings</Button>}>
<Dialog.CloseButton />
<Dialog.Title render={<Heading size='lg' />}>Settings</Dialog.Title>

<div style={{ display: 'flex', flex: 1, gap: '1.5rem', minHeight: 0 }}>
<nav style={{ flex: 'none', width: '12rem' }}>…</nav>

<div {...stylex.props(scrollAreaRoot)} style={{ flex: 1, minWidth: 0 }}>
<div {...stylex.props(...scrollAreaViewport())}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 2 "import \\* as stylex from '`@stylexjs/stylex`'|stylex\\.props" packages/ui packages/swingset \
  -g '*.ts' -g '*.tsx' -g '*.mdx'

Repository: clerk/javascript

Length of output: 46756


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '260,295p' packages/swingset/src/stories/dialog.component.mdx
printf '\n--- stylex references and imports in this story ---\n'
rg -n "stylex|scrollAreaRoot|scrollAreaViewport" packages/swingset/src/stories/dialog.component.mdx

Repository: clerk/javascript

Length of output: 2119


Import stylex in the panel example.

The example calls stylex.props without defining stylex, so copied code fails with an unresolved identifier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swingset/src/stories/dialog.component.mdx` around lines 275 - 286,
Add the missing stylex import to the panel example before its usage in the
Dialog content, ensuring the existing stylex.props calls resolve when the
snippet is copied.

Source: Coding guidelines

Comment on lines +541 to +557
it('keeps one meta for stacked dialogs and removes it only with the last', async () => {
const user = userEvent.setup();
render(
<Dialog defaultOpen>
<div>Outer</div>
<Dialog trigger={addEmailTriggerShared}>
<div>Inner</div>
</Dialog>
</Dialog>,
);

await user.click(screen.getByRole('button', { name: 'Add email' }));
expect(document.head.querySelectorAll('meta[name="theme-color"]')).toHaveLength(1);

await user.keyboard('{Escape}');
expect(themeColor()).not.toBeNull();
});

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the stacked-dialog teardown assertion.

The test name states that the meta is removed only with the last dialog. The test closes the inner dialog and asserts the meta still exists. It never closes the outer dialog, so the "removes it only with the last" half is unverified. Add a second Escape and assert removal. Use waitFor, because removal is deferred until after the fade.

💚 Proposed addition
     await user.keyboard('{Escape}');
     expect(themeColor()).not.toBeNull();
+
+    await user.keyboard('{Escape}');
+    await waitFor(() => expect(themeColor()).toBeNull());
   });
📝 Committable suggestion

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

Suggested change
it('keeps one meta for stacked dialogs and removes it only with the last', async () => {
const user = userEvent.setup();
render(
<Dialog defaultOpen>
<div>Outer</div>
<Dialog trigger={addEmailTriggerShared}>
<div>Inner</div>
</Dialog>
</Dialog>,
);
await user.click(screen.getByRole('button', { name: 'Add email' }));
expect(document.head.querySelectorAll('meta[name="theme-color"]')).toHaveLength(1);
await user.keyboard('{Escape}');
expect(themeColor()).not.toBeNull();
});
it('keeps one meta for stacked dialogs and removes it only with the last', async () => {
const user = userEvent.setup();
render(
<Dialog defaultOpen>
<div>Outer</div>
<Dialog trigger={addEmailTriggerShared}>
<div>Inner</div>
</Dialog>
</Dialog>,
);
await user.click(screen.getByRole('button', { name: 'Add email' }));
expect(document.head.querySelectorAll('meta[name="theme-color"]')).toHaveLength(1);
await user.keyboard('{Escape}');
expect(themeColor()).not.toBeNull();
await user.keyboard('{Escape}');
await waitFor(() => expect(themeColor()).toBeNull());
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/dialog.test.tsx` around lines 541 -
557, Complete the test around the stacked-dialog teardown flow by sending a
second Escape after the existing inner-dialog assertion, then use waitFor to
assert that themeColor() becomes null after the outer dialog closes and deferred
fade cleanup finishes.

Source: Coding guidelines

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 7

🧹 Nitpick comments (10)
packages/ui/src/mosaic/components/dialog/keyboard-inset.ts (1)

75-82: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the release function idempotent.

Each call to the returned function decrements listeners. A second call on the same release drives the count below zero. The counter then never returns to 0, and the listeners plus the --_cl-keyboard-inset property stay attached for the lifetime of the page. acquireBrowserChrome in browser-chrome.ts guards this case with a released flag; this module does not.

♻️ Proposed guard
-  return () => {
-    listeners--;
-    if (listeners === 0 && detach) {
-      detach();
-      detach = null;
-    }
-  };
+  let released = false;
+  return () => {
+    if (released) {
+      return;
+    }
+    released = true;
+    listeners--;
+    if (listeners === 0 && detach) {
+      detach();
+      detach = null;
+    }
+  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/keyboard-inset.ts` around lines 75 -
82, Make the release function returned by the keyboard-inset acquisition flow
idempotent by adding a per-release guard, similar to acquireBrowserChrome’s
released flag. Only decrement listeners and detach the keyboard-inset
listener/property when that release has not already been executed.
packages/ui/src/mosaic/components/dialog/dialog.tsx (1)

205-229: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

render in rest silently replaces the styled Button.

CloseButton sets render before {...rest}. If a consumer passes render, their element replaces the Button wrapper, and styles.closeButton plus closeInsets[size] are lost. The button then loses its absolute anchoring. Consider omitting render from DialogCloseButtonProps, or documenting that the override must supply its own positioning.

♻️ Proposed type change
-export interface DialogCloseButtonProps extends MosaicComponentProps<'button'> {
+export interface DialogCloseButtonProps extends Omit<MosaicComponentProps<'button'>, 'render'> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/dialog.tsx` around lines 205 - 229,
Prevent consumers from overriding the internal render used by CloseButton: omit
render from DialogCloseButtonProps and exclude it from the rest props spread,
preserving the styled Button with styles.closeButton and closeInsets[size].
packages/ui/src/mosaic/components/dialog/dialog.styles.ts (1)

199-232: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Trim the duplicated and oversized comments in sizes.panel.

Two blocks state the same fact. Lines 214-223 explain that the panel fills the viewport content box with stretch, and lines 224-228 repeat it. Reduce the block to a single terse note. The same applies across this file, where multi-paragraph rationale blocks dominate the style declarations.

The coding guidelines require minimal comments: "Add comments only when critical to explain why a non-obvious change was made; never restate code behavior, and keep warranted comments to one terse line rather than a verbose multi-line block."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/dialog.styles.ts` around lines 199 -
232, Trim the duplicated rationale comments in sizes.panel, especially the
repeated explanation around alignSelf: 'stretch', leaving one concise line only
where the non-obvious layout decision requires justification. Apply the same
minimal-comment standard to nearby oversized rationale blocks in this file
without changing the style declarations.

Source: Coding guidelines

packages/ui/src/mosaic/components/dialog/browser-chrome.ts (2)

160-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The comment describes a binary search; the code performs a linear scan.

makeEasing walks the table with while (lo < SAMPLES && table[lo + 1] < x) lo++. That is a linear scan, not a binary search. Correct the comment or implement the search that it describes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/browser-chrome.ts` around lines 160
- 190, Update makeEasing so its lookup uses a binary search over the monotonic
table rather than incrementing lo through entries linearly; preserve the
existing interpolation and axis calculation behavior.

229-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The acquireBrowserChrome doc block is attached to resolveTint.

Lines 229-237 document acquireBrowserChrome and its @param backdrop. A second doc block for resolveTint follows at lines 238-243, and the function resolveTint declaration follows that. TypeScript and editors therefore associate the first block with nothing, and acquireBrowserChrome at line 259 has no JSDoc. Move the first block directly above export function acquireBrowserChrome.

The coding guidelines require that "All public APIs must be documented with JSDoc".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/dialog/browser-chrome.ts` around lines 229
- 257, Move the first JSDoc block describing the refcounted backdrop behavior
and its backdrop parameter from above resolveTint to directly above export
function acquireBrowserChrome. Keep the separate resolveTint documentation
attached to resolveTint, ensuring the public acquireBrowserChrome API retains
its documentation.

Source: Coding guidelines

packages/ui/src/mosaic/styles/index.ts (1)

17-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Re-export the headless dialog types that the exported prop types reference.

packages/ui/src/mosaic/components/dialog/index.ts also exports DialogFocusTarget, DialogHandle, and DialogOpenChangeDetails. This barrel omits them. DialogRootProps and DialogTriggerProps carry a handle?: DialogHandle<Payload> member, and DialogPopupProps carries initialFocus/finalFocus of type DialogFocusTarget. A consumer of this entry point can therefore pass those props but cannot name their types.

♻️ Proposed addition
 export { Dialog } from '../components/dialog';
 export type {
   DialogBackdropProps,
   DialogCloseButtonProps,
   DialogCloseProps,
   DialogDescriptionProps,
+  DialogFocusTarget,
+  DialogHandle,
+  DialogOpenChangeDetails,
   DialogPopupProps,
   DialogProps,
   DialogRootProps,
   DialogSize,
   DialogTitleProps,
   DialogTriggerProps,
   DialogViewportProps,
 } from '../components/dialog';
🤖 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/styles/index.ts` around lines 17 - 30, Update the
dialog type re-exports in the styles barrel to include DialogFocusTarget,
DialogHandle, and DialogOpenChangeDetails from the existing dialog component
exports, alongside the current DialogRootProps, DialogTriggerProps, and
DialogPopupProps types.
packages/headless/src/primitives/dialog/dialog.test.tsx (1)

385-455: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for a Dialog.Trigger with neither a root nor a handle.

dialog-trigger.tsx line 36 throws a documented error for this case. No test covers it. The test also exposes the hook-order problem flagged in packages/headless/src/primitives/dialog/dialog-trigger.tsx lines 33-37, because React reports a hook-count error instead of the intended message once a store disappears between renders.

💚 Proposed test
+    it('throws when the trigger has neither a root nor a handle', () => {
+      const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
+      expect(() => render(<Dialog.Trigger>Orphan</Dialog.Trigger>)).toThrow(
+        /must be nested in a <Dialog.Root> or given a `handle`/,
+      );
+      consoleError.mockRestore();
+    });
🤖 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/headless/src/primitives/dialog/dialog.test.tsx` around lines 385 -
455, Add a test in the detached-trigger describe block that renders
Dialog.Trigger without a Dialog.Root or handle and asserts the documented error
from Dialog.Trigger. Include a rerender or unmount scenario where the associated
store disappears to verify stable hook ordering and ensure the intended error is
reported instead of a React hook-count error.

Source: Coding guidelines

packages/headless/src/primitives/dialog/dialog-root.tsx (1)

121-143: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Confirm the stability assumptions in the setRoot effect.

The effect only re-runs when store changes. openFromTrigger and closeFromTrigger capture refs, floatingContext, and setActiveTriggerId from the render that attached the controller. setActiveTriggerId comes from useControllableState, whose setter identity depends on isControlled. If a consumer switches triggerId between undefined and a value after mount, the captured setter becomes stale and trigger attribution stops updating. applyOpenChange already avoids this through the latest ref; consider routing setActiveTriggerId and setActivePayload through the same ref.

♻️ Proposed change to route trigger state through the latest ref
-  const latest = useRef({ applyOpenChange, activeTriggerId });
+  const latest = useRef({ applyOpenChange, activeTriggerId, setActiveTriggerId, setActivePayload });
   useLayoutEffect(() => {
-    latest.current = { applyOpenChange, activeTriggerId };
+    latest.current = { applyOpenChange, activeTriggerId, setActiveTriggerId, setActivePayload };
   });
 
   useLayoutEffect(() => {
     return store.setRoot({
       openFromTrigger: (id, event) => {
         const registration = store.getTrigger(id);
-        setActiveTriggerId(id);
-        setActivePayload(registration?.payload);
+        latest.current.setActiveTriggerId(id);
+        latest.current.setActivePayload(registration?.payload);
🤖 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/headless/src/primitives/dialog/dialog-root.tsx` around lines 121 -
143, Update the setRoot effect callbacks in the dialog root to access
setActiveTriggerId and setActivePayload through the latest ref, matching the
existing latest.current.applyOpenChange pattern. Ensure openFromTrigger always
uses the current controllable-state setters when triggerId control changes,
while preserving the existing trigger registration, reference assignment,
pending details, and open-change behavior.
packages/headless/src/primitives/dialog/dialog-handle.ts (1)

59-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused getRegistryVersion member.

dialog-root.tsx re-resolves through store.subscribe, and no caller reads getRegistryVersion. Remove the member, counter, and getter.

🤖 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/headless/src/primitives/dialog/dialog-handle.ts` around lines 59 -
60, Remove the unused getRegistryVersion() member from the dialog handle
contract, along with the registry version counter and its getter implementation.
Preserve the existing store.subscribe-based re-resolution in dialog-root.tsx and
remove only the obsolete registry-version plumbing.
packages/headless/src/primitives/dialog/use-dialog-origin.ts (1)

34-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Condense the two explanatory comment blocks.

The coding guidelines state: "keep warranted comments to one terse line rather than a verbose multi-line block". The measurement reasoning is worth recording, but six-line and four-line blocks exceed that. Reduce each to one line, or move the full rationale into the function JSDoc at Lines 8-19.

Also applies to: 50-53

🤖 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/headless/src/primitives/dialog/use-dialog-origin.ts` around lines 34
- 39, Condense the explanatory comment blocks surrounding the measurement logic
in use-dialog-origin, including the blocks near lines 34-39 and 50-53, to one
terse line each. Preserve the essential rationale about scaled
getBoundingClientRect values, unscaled offset dimensions, and transform-origin
coordinates, without changing the implementation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/olive-doors-tell.md:
- Around line 2-3: Update the Dialog changes in the relevant UI and headless
compatibility implementations to retain deprecated support for the removed sx
and existing size APIs, preserving consumers on `@clerk/ui`@1 during this patch
release. If compatibility cannot be preserved, change the changeset entries for
`@clerk/headless` and `@clerk/ui` from patch to major and document the migration
path.

In `@packages/headless/src/primitives/dialog/dialog-root.tsx`:
- Around line 117-178: Add an SSR-safe layout-effect helper in the dialog
primitives and use it for every useLayoutEffect shown in DialogInner, including
the latest ref, root registration, reference resolution, payload lookup, state
publication, and cleanup effects. Preserve each effect’s dependencies, cleanup
behavior, and execution order while replacing the direct layout-effect usage
with the helper.

In `@packages/headless/src/primitives/dialog/README.md`:
- Around line 151-171: Qualify the dialog dismissal documentation to reflect
that Escape and outside-press dismissal depend on closedBy: in
packages/headless/src/primitives/dialog/README.md lines 151-171, state that
Escape closes only when closedBy is not 'none'; in
packages/swingset/src/stories/dialog.component.mdx lines 54-55, replace the
unconditional “always” wording with behavior conditional on the default
closedBy='any' value.
- Around line 105-108: Update the initialFocus and finalFocus callback
documentation for Dialog.Popup to remove refs from the callback return options.
Document callback results as boolean, void, HTMLElement, or null, while keeping
refs listed only as supported direct values.

In `@packages/headless/src/primitives/dialog/use-dialog-origin.ts`:
- Around line 25-54: Reset the popup’s ORIGIN_PROPERTY to the neutral center
value before calling getBoundingClientRect() in the useLayoutEffect, ensuring
reused popups are measured without the previous transform origin. Keep the
existing open/popup/trigger guards and origin calculation unchanged, then set
the computed origin afterward.

In `@packages/swingset/src/stories/dialog.component.mdx`:
- Around line 275-286: Add the missing stylex import to the panel example before
its usage in the Dialog content, ensuring the existing stylex.props calls
resolve when the snippet is copied.

In `@packages/ui/src/mosaic/components/dialog/dialog.test.tsx`:
- Around line 541-557: Complete the test around the stacked-dialog teardown flow
by sending a second Escape after the existing inner-dialog assertion, then use
waitFor to assert that themeColor() becomes null after the outer dialog closes
and deferred fade cleanup finishes.

---

Nitpick comments:
In `@packages/headless/src/primitives/dialog/dialog-handle.ts`:
- Around line 59-60: Remove the unused getRegistryVersion() member from the
dialog handle contract, along with the registry version counter and its getter
implementation. Preserve the existing store.subscribe-based re-resolution in
dialog-root.tsx and remove only the obsolete registry-version plumbing.

In `@packages/headless/src/primitives/dialog/dialog-root.tsx`:
- Around line 121-143: Update the setRoot effect callbacks in the dialog root to
access setActiveTriggerId and setActivePayload through the latest ref, matching
the existing latest.current.applyOpenChange pattern. Ensure openFromTrigger
always uses the current controllable-state setters when triggerId control
changes, while preserving the existing trigger registration, reference
assignment, pending details, and open-change behavior.

In `@packages/headless/src/primitives/dialog/dialog.test.tsx`:
- Around line 385-455: Add a test in the detached-trigger describe block that
renders Dialog.Trigger without a Dialog.Root or handle and asserts the
documented error from Dialog.Trigger. Include a rerender or unmount scenario
where the associated store disappears to verify stable hook ordering and ensure
the intended error is reported instead of a React hook-count error.

In `@packages/headless/src/primitives/dialog/use-dialog-origin.ts`:
- Around line 34-39: Condense the explanatory comment blocks surrounding the
measurement logic in use-dialog-origin, including the blocks near lines 34-39
and 50-53, to one terse line each. Preserve the essential rationale about scaled
getBoundingClientRect values, unscaled offset dimensions, and transform-origin
coordinates, without changing the implementation.

In `@packages/ui/src/mosaic/components/dialog/browser-chrome.ts`:
- Around line 160-190: Update makeEasing so its lookup uses a binary search over
the monotonic table rather than incrementing lo through entries linearly;
preserve the existing interpolation and axis calculation behavior.
- Around line 229-257: Move the first JSDoc block describing the refcounted
backdrop behavior and its backdrop parameter from above resolveTint to directly
above export function acquireBrowserChrome. Keep the separate resolveTint
documentation attached to resolveTint, ensuring the public acquireBrowserChrome
API retains its documentation.

In `@packages/ui/src/mosaic/components/dialog/dialog.styles.ts`:
- Around line 199-232: Trim the duplicated rationale comments in sizes.panel,
especially the repeated explanation around alignSelf: 'stretch', leaving one
concise line only where the non-obvious layout decision requires justification.
Apply the same minimal-comment standard to nearby oversized rationale blocks in
this file without changing the style declarations.

In `@packages/ui/src/mosaic/components/dialog/dialog.tsx`:
- Around line 205-229: Prevent consumers from overriding the internal render
used by CloseButton: omit render from DialogCloseButtonProps and exclude it from
the rest props spread, preserving the styled Button with styles.closeButton and
closeInsets[size].

In `@packages/ui/src/mosaic/components/dialog/keyboard-inset.ts`:
- Around line 75-82: Make the release function returned by the keyboard-inset
acquisition flow idempotent by adding a per-release guard, similar to
acquireBrowserChrome’s released flag. Only decrement listeners and detach the
keyboard-inset listener/property when that release has not already been
executed.

In `@packages/ui/src/mosaic/styles/index.ts`:
- Around line 17-30: Update the dialog type re-exports in the styles barrel to
include DialogFocusTarget, DialogHandle, and DialogOpenChangeDetails from the
existing dialog component exports, alongside the current DialogRootProps,
DialogTriggerProps, and DialogPopupProps types.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: e06bfa40-bf96-4071-89f5-c32a12b34f03

📥 Commits

Reviewing files that changed from the base of the PR and between 89be03b and af5f90d.

📒 Files selected for processing (37)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/dialog/use-dialog-origin.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.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/cli (auto-detected)
  • clerk/clerk-ios (auto-detected)
  • clerk/clerk-android (auto-detected)
💤 Files with no reviewable changes (2)
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
🛑 Comments failed to post (1)
packages/headless/src/primitives/dialog/README.md (1)

151-171: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document dismissal as conditional on closedBy.

closedBy='none' disables both Escape and outside-press dismissal. The current documentation makes unconditional dismissal claims.

  • packages/headless/src/primitives/dialog/README.md#L151-L171: qualify the Keyboard section so Escape closes only when closedBy is not 'none'.
  • packages/swingset/src/stories/dialog.component.mdx#L54-L55: replace “always” with behavior conditional on the default closedBy='any' value.
📍 Affects 2 files
  • packages/headless/src/primitives/dialog/README.md#L151-L171 (this comment)
  • packages/swingset/src/stories/dialog.component.mdx#L54-L55
🤖 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/headless/src/primitives/dialog/README.md` around lines 151 - 171,
Qualify the dialog dismissal documentation to reflect that Escape and
outside-press dismissal depend on closedBy: in
packages/headless/src/primitives/dialog/README.md lines 151-171, state that
Escape closes only when closedBy is not 'none'; in
packages/swingset/src/stories/dialog.component.mdx lines 54-55, replace the
unconditional “always” wording with behavior conditional on the default
closedBy='any' value.

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.

1 participant