From 5ad9bab2782038c4c13046c9d98bc9e0cef4ad4a Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:12:06 +1000 Subject: [PATCH 1/2] fix(app): stop :has() rules restyling every ancestor on DOM insertion --- packages/app/src/settings/settings.css | 10 ++++++++-- packages/ui/src/components/image-preview.css | 5 +++-- packages/ui/src/components/image-preview.tsx | 4 ++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/app/src/settings/settings.css b/packages/app/src/settings/settings.css index 3f61e0ac2de2..9b3c9633410d 100644 --- a/packages/app/src/settings/settings.css +++ b/packages/app/src/settings/settings.css @@ -846,16 +846,22 @@ justify-content: flex-end; } -[data-slot="settings-row-control"] > div:has([data-component="switch"]), +/* The :has() belongs on the control, not on its div. Chromium checks a div:has() subject against every div in the + document and flags each one, then restyles a flagged div whenever an element is inserted anywhere beneath it, which + is every ancestor of a streaming answer. */ +[data-slot="settings-row-control"]:has(> div [data-component="switch"]) > div, [data-slot="settings-row-control"] > [data-component="switch"] { display: inline-flex; align-items: center; padding: 4px; } +[data-component="settings-list"][data-variant="catalog"] + [data-slot="settings-row-control"]:has(> div [data-component="switch"]) + > div, [data-component="settings-list"][data-variant="catalog"] [data-slot="settings-row-control"] - > :is(div:has([data-component="switch"]), [data-component="switch"]) { + > [data-component="switch"] { padding: 0; } diff --git a/packages/ui/src/components/image-preview.css b/packages/ui/src/components/image-preview.css index d69b4feca80a..6458b9c2dda8 100644 --- a/packages/ui/src/components/image-preview.css +++ b/packages/ui/src/components/image-preview.css @@ -182,8 +182,9 @@ } /* A modal Kobalte dialog turns the body's pointer events off and only restores them when its content unmounts, which - the closing flight above holds back; the page takes clicks again as soon as the close starts. */ -body:has([data-component="image-preview"] [data-slot="image-preview-content"][data-closed]) { + the closing flight above holds back; the page takes clicks again as soon as the close starts. The preview marks the + body itself: a body:has() rule made Chromium restyle the body on every DOM insertion anywhere in the page. */ +body[data-image-preview-closing] { pointer-events: auto !important; } diff --git a/packages/ui/src/components/image-preview.tsx b/packages/ui/src/components/image-preview.tsx index 880ba6500548..69b43e55f10e 100644 --- a/packages/ui/src/components/image-preview.tsx +++ b/packages/ui/src/components/image-preview.tsx @@ -417,6 +417,10 @@ export function ImagePreview(props: ImagePreviewProps) { ), ) + // See image-preview.css: the body takes clicks again while the closing flight holds the dialog mounted. + createEffect(() => document.body.toggleAttribute("data-image-preview-closing", !dialog.isOpen())) + onCleanup(() => document.body.removeAttribute("data-image-preview-closing")) + // From the center of the box zoom is measured in: the window's for an image, the frame's for a video. const point = (event: MouseEvent) => { const rect = (props.video ? image : viewport).getBoundingClientRect() From 82067119c0ba34af4e469bef11264f5d7ed48628 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:15:05 +1000 Subject: [PATCH 2/2] refactor(ui): mark a closing preview from its existing close effect --- packages/ui/src/components/image-preview.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/image-preview.tsx b/packages/ui/src/components/image-preview.tsx index 69b43e55f10e..42d1bd95b54c 100644 --- a/packages/ui/src/components/image-preview.tsx +++ b/packages/ui/src/components/image-preview.tsx @@ -405,22 +405,22 @@ export function ImagePreview(props: ImagePreviewProps) { settle() showOrigin() giveBack() + document.body.removeAttribute("data-image-preview-closing") }) createEffect( on( () => dialog.isOpen(), (open) => { + // See image-preview.css: the body takes clicks again while the closing flight holds the dialog mounted. + document.body.toggleAttribute("data-image-preview-closing", !open) + if (!open) dismiss() }, { defer: true }, ), ) - // See image-preview.css: the body takes clicks again while the closing flight holds the dialog mounted. - createEffect(() => document.body.toggleAttribute("data-image-preview-closing", !dialog.isOpen())) - onCleanup(() => document.body.removeAttribute("data-image-preview-closing")) - // From the center of the box zoom is measured in: the window's for an image, the frame's for a video. const point = (event: MouseEvent) => { const rect = (props.video ? image : viewport).getBoundingClientRect()