fix(desktop): dedupe react-dismissable-layer to stop modal menu → dialog freezes (#1899)

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matt Toohey
2026-07-16 00:34:50 +10:00
committed by GitHub
parent 40fe650958
commit 366567ac20
6 changed files with 170 additions and 101 deletions

View File

@@ -69,6 +69,7 @@ export default defineConfig({
"**/threadpane-ultrawide.spec.ts",
"**/animated-avatar.spec.ts",
"**/reminders.spec.ts",
"**/reminder-click-repro.spec.ts",
"**/virtualization.spec.ts",
"**/scroll-history.spec.ts",
"**/channel-dense-second-reach.spec.ts",

View File

@@ -411,11 +411,7 @@ export function ChannelGroupSection({
items.length > 0 ? (
<SidebarMenu data-testid={listTestId}>
{items.map((channel) => (
// modal={false}: menu items (e.g. Leave channel) open a modal
// AlertDialog. A modal ContextMenu would leave `pointer-events: none`
// stuck on <body> when it closes as the dialog mounts, freezing the
// whole app. Non-modal avoids installing that body guard entirely.
<ContextMenu key={channel.id} modal={false}>
<ContextMenu key={channel.id}>
<ContextMenuTrigger asChild>
<SidebarMenuItem className="content-visibility-auto-row">
{draggable ? (
@@ -610,10 +606,7 @@ export function CustomChannelSection({
)}
data-section-actions-open={actionsMenuOpen || undefined}
>
{/* modal={false}: Rename/Delete section open a modal dialog;
a modal ContextMenu would leave `pointer-events: none` stuck on
<body> after it closes, freezing the app. */}
<ContextMenu modal={false}>
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="relative" {...dragHandleProps}>
<SidebarGroupLabel asChild>
@@ -702,10 +695,7 @@ export function CustomChannelSection({
{channels.length > 0 ? (
<SidebarMenu>
{channels.map((channel) => (
// modal={false}: see note on the other channel ContextMenu
// above — avoids the pointer-events lockup when Leave
// channel's AlertDialog opens.
<ContextMenu key={channel.id} modal={false}>
<ContextMenu key={channel.id}>
<ContextMenuTrigger asChild>
<SidebarMenuItem>
<DraggableChannelRow channelId={channel.id}>

View File

@@ -0,0 +1,144 @@
import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
/**
* Regression suite for the "app freezes after setting a reminder" bug.
*
* The app used to bundle two copies of @radix-ui/react-dismissable-layer
* (react-menu resolved 1.1.11 while react-dialog resolved 1.1.15). Each copy
* keeps its own module-level `originalBodyPointerEvents`, so when a *modal*
* menu opened a *modal* dialog in the same React commit, the dialog's copy
* saved the menu's `pointer-events: none` as the "original" body style and
* restored it when the dialog closed — leaving `pointer-events: none` stuck
* on <body> and deadening the whole app to the mouse.
*
* The inbox row context menu hit this: its "Remind me later" item opens
* RemindMeLaterDialog synchronously. Fixed by deduplicating the layer via
* the pnpm override in pnpm-workspace.yaml — a single copy coordinates
* nested modal layers correctly, so the menu keeps its default modal
* behavior.
*
* Each test drives one reminder entry point through a full set-reminder
* cycle, then asserts <body> pointer-events is not stuck and that a real
* click still lands. The inbox right-click test fails if the dismissable
* layer ever duplicates again; the message-menu tests pin the already-safe
* paths.
*/
const GENERAL_MESSAGE_ROW = "message-row";
// The default mock feed's mention row (from alice, in #general) has a
// channel target, so its "Remind me later" context-menu item is enabled.
const INBOX_MENTION_ROW = "home-inbox-item-mock-feed-mention";
/**
* Assert the app survived the menu → dialog → close cycle.
*
* The stuck state is inline `pointer-events: none` left on <body>. The buggy
* restore runs when the dialog's dismissable layer unmounts — after its exit
* animation — so first wait for the dialog to leave the DOM, then check the
* body style, then prove clicks still land by navigating via the sidebar.
*/
async function expectAppAliveAfterDialogClose(
page: import("@playwright/test").Page,
) {
await expect(page.getByRole("dialog")).toHaveCount(0);
await waitForAnimations(page);
expect(await page.evaluate(() => document.body.style.pointerEvents)).not.toBe(
"none",
);
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
}
/** Open the Remind-me-later dialog from a message row's More-actions menu. */
async function openReminderDialogFromMessageMenu(
page: import("@playwright/test").Page,
) {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const messageRow = page.getByTestId(GENERAL_MESSAGE_ROW).first();
await messageRow.hover();
await messageRow.getByRole("button", { name: "More actions" }).click();
const remindItem = page.getByRole("menuitem", { name: "Remind me later" });
await expect(remindItem).toBeVisible();
await waitForAnimations(page);
await remindItem.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await waitForAnimations(page);
return dialog;
}
async function expectReminderSetToast(page: import("@playwright/test").Page) {
await expect(
page.locator("[data-sonner-toast]").filter({ hasText: "Reminder set" }),
).toBeVisible();
}
test.describe("reminder set → app stays clickable", () => {
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
test("01 — message menu → time preset", async ({ page }) => {
const dialog = await openReminderDialogFromMessageMenu(page);
await dialog.getByRole("button", { name: "In 30 minutes" }).click();
await expectReminderSetToast(page);
await expectAppAliveAfterDialogClose(page);
});
test("02 — message menu → custom time via Set reminder footer button", async ({
page,
}) => {
const dialog = await openReminderDialogFromMessageMenu(page);
// The custom timestamp must be strictly in the future or the Set
// reminder button stays disabled — pick tomorrow at 09:00 local time.
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
const date = [
tomorrow.getFullYear(),
String(tomorrow.getMonth() + 1).padStart(2, "0"),
String(tomorrow.getDate()).padStart(2, "0"),
].join("-");
await dialog.getByLabel("Reminder date").fill(date);
await dialog.getByLabel("Reminder time").fill("09:00");
await dialog.getByRole("button", { name: "Set reminder" }).click();
await expectReminderSetToast(page);
await expectAppAliveAfterDialogClose(page);
});
// The regression path: only the inbox right-click stacks a modal
// ContextMenu under the modal dialog. Fails if two copies of the
// dismissable layer are ever bundled again — body pointer-events stays
// "none" after the dialog closes and the sidebar click below times out.
test("03 — inbox row right-click → Remind me later", async ({ page }) => {
await page.goto("/");
await expect(page.getByTestId("home-inbox")).toBeVisible();
const row = page.getByTestId(INBOX_MENTION_ROW);
await expect(row).toBeVisible();
await row.click({ button: "right" });
const remindItem = page.getByRole("menuitem", { name: "Remind me later" });
await expect(remindItem).toBeVisible();
await waitForAnimations(page);
await remindItem.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await waitForAnimations(page);
await dialog.getByRole("button", { name: "In 30 minutes" }).click();
await expectReminderSetToast(page);
await expectAppAliveAfterDialogClose(page);
});
});

View File

@@ -22,10 +22,12 @@ async function storedSidebarWidth(page: Page) {
);
}
// Regression guard for the "Leave channel" lockup: opening a modal AlertDialog
// from a modal Radix ContextMenu leaves `pointer-events: none` stuck on <body>
// after the dialog closes, freezing the whole app. The fix makes the sidebar
// context menus non-modal. This asserts the app is still interactive.
// Regression guard for the "Leave channel" lockup: with two bundled copies of
// @radix-ui/react-dismissable-layer, opening a modal AlertDialog from a modal
// Radix ContextMenu left `pointer-events: none` stuck on <body> after the
// dialog closed, freezing the whole app. Fixed by the pnpm override in
// pnpm-workspace.yaml deduplicating the layer. This asserts the app is still
// interactive.
async function expectAppClickable(page: Page) {
await expect
.poll(() =>

92
pnpm-lock.yaml generated
View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
'@radix-ui/react-dismissable-layer': 1.1.15
patchedDependencies:
isomorphic-git: e9b414a60d4cf1d8aa18f7a779483984e821989967c235662566e94ef0238d3f
virtua@0.49.3: 367ec28b983b840021685fb5b515df23616a4d9b500614483ada43907c1e14e2
@@ -72,7 +75,7 @@ importers:
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.5(@types/react@19.2.17)(react@19.2.7)
version: 1.3.0(@types/react@19.2.17)(react@19.2.7)
'@radix-ui/react-switch':
specifier: ^1.2.6
version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -250,7 +253,7 @@ importers:
version: 4.6.2
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.5(@types/react@19.2.17)(react@19.2.7)
version: 1.3.0(@types/react@19.2.17)(react@19.2.7)
'@radix-ui/react-tooltip':
specifier: ^1.2.8
version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -455,21 +458,18 @@ packages:
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@biomejs/cli-linux-x64-musl@2.4.16':
resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-x64@2.4.16':
resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@biomejs/cli-win32-arm64@2.4.16':
resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==}
@@ -523,15 +523,9 @@ packages:
emoji-mart: ^5.2
react: ^16.8 || ^17 || ^18
'@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
'@floating-ui/core@1.8.0':
resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
'@floating-ui/dom@1.7.6':
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
'@floating-ui/dom@1.8.0':
resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
@@ -541,9 +535,6 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
@@ -792,19 +783,6 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.1.11':
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-dismissable-layer@1.1.15':
resolution: {integrity: sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA==}
peerDependencies:
@@ -1221,15 +1199,6 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.1.1':
resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-is-hydrated@0.1.1':
resolution: {integrity: sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==}
peerDependencies:
@@ -1365,21 +1334,18 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.2':
resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.2':
resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.2':
resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==}
@@ -1393,7 +1359,6 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.2':
resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==}
@@ -1513,21 +1478,18 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.3.0':
resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.3.0':
resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.3.0':
resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==}
@@ -1665,7 +1627,6 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.11.2':
resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==}
@@ -1693,7 +1654,6 @@ packages:
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.11.2':
resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==}
@@ -2403,7 +2363,6 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
@@ -2417,7 +2376,6 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
@@ -3134,7 +3092,7 @@ packages:
engines: {node: '>= 0.4'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -3349,21 +3307,10 @@ snapshots:
emoji-mart: 5.6.0
react: 19.2.7
'@floating-ui/core@1.7.5':
dependencies:
'@floating-ui/utils': 0.2.11
optional: true
'@floating-ui/core@1.8.0':
dependencies:
'@floating-ui/utils': 0.2.12
'@floating-ui/dom@1.7.6':
dependencies:
'@floating-ui/core': 1.7.5
'@floating-ui/utils': 0.2.11
optional: true
'@floating-ui/dom@1.8.0':
dependencies:
'@floating-ui/core': 1.8.0
@@ -3375,9 +3322,6 @@ snapshots:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
'@floating-ui/utils@0.2.11':
optional: true
'@floating-ui/utils@0.2.12': {}
'@fontsource-variable/inter@5.2.8': {}
@@ -3601,19 +3545,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.17
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.7)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.17)(react@19.2.7)
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.17)(react@19.2.7)
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
optionalDependencies:
'@types/react': 19.2.17
'@types/react-dom': 19.2.3(@types/react@19.2.17)
'@radix-ui/react-dismissable-layer@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@radix-ui/primitive': 1.1.5
@@ -3697,7 +3628,7 @@ snapshots:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.7)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.17)(react@19.2.7)
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.17)(react@19.2.7)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.17)(react@19.2.7)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.17)(react@19.2.7)
@@ -4037,13 +3968,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.17
'@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.17)(react@19.2.7)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.17)(react@19.2.7)
react: 19.2.7
optionalDependencies:
'@types/react': 19.2.17
'@radix-ui/react-use-is-hydrated@0.1.1(@types/react@19.2.17)(react@19.2.7)':
dependencies:
react: 19.2.7
@@ -4464,7 +4388,7 @@ snapshots:
'@tiptap/extension-bubble-menu@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)':
dependencies:
'@floating-ui/dom': 1.7.6
'@floating-ui/dom': 1.8.0
'@tiptap/core': 3.22.5(@tiptap/pm@3.22.5)
'@tiptap/pm': 3.22.5
optional: true

View File

@@ -3,6 +3,14 @@ packages:
- "web"
allowBuilds:
esbuild: true
overrides:
# Force a single copy of the dismissable-layer. Radix packages otherwise
# resolve different versions (react-menu pulled 1.1.11, react-dialog 1.1.15),
# and each copy keeps its own module-level saved <body> pointer-events style —
# so a modal menu opening a modal dialog left `pointer-events: none` stuck on
# <body> when the dialog closed, freezing the app (#1482, reminder dialog).
# Removable once every @radix-ui dep converges on one version naturally.
"@radix-ui/react-dismissable-layer": 1.1.15
patchedDependencies:
isomorphic-git: patches/isomorphic-git.patch
virtua@0.49.3: patches/virtua@0.49.3.patch