fix(panels): mount the panel host inside the shell's framed content viewport
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s

On DSH Desktop the plugin's panel layer covered host modals — the community
market dialog (dsh-community-market, which renders into the shell's own
`shell.overlay` slot) had its lower-left corner clipped by our workbench.

The fence is not a z-index mistake on either side. The shell's framed modes do:

  const contentViewport = document.getElementById('root')
  document.body.dataset.dshDesktopMode = environment.mode
  /* …and tags #root with data-dsh-desktop-content-viewport */

with

  body:is([data-dsh-desktop-mode="compatibility"], […"extended"]) [data-dsh-desktop-content-viewport] {
    isolation: isolate;
  }

`isolation: isolate` makes #root a stacking context (z-index: auto) wrapping the
whole app INCLUDING the shell's overlay layer (`.dshDesktopOverlay`,
z-index: 1000) that hosts plugin overlays. Our host was appended to
document.body — OUTSIDE that context — and because it is a positioned element
with z-index 25 it painted above the entire context, so the market's 1000 never
competed. The shell's overlay layer is the boundary plugins are meant to sit
under, so the fix is to join the layer, not to renumber ours: panelHostParent()
mounts the host into #root when the shell framed one (body otherwise, i.e. the
plain web deployment keeps today's behaviour), where the documented 25 keeps
its meaning — above the app frame (z-index auto), below the overlay layer.

The re-anchor guard now re-appends into the resolved parent instead of always
`document.body`, and also watches `data-dsh-desktop-mode` on <body> (the same
shell effect that stamps the viewport) so a shell that frames #root after our
mount still gets the host moved.

Verified: typecheck clean; build clean; 1367 passed / 9 skipped (131 files,
+3 parenting specs). Revert is one line per call site if the shell's overlay
layer turns out to sit below our panels for a reason not visible in the CSS.
This commit is contained in:
figmar
2026-10-04 00:08:13 +08:00
parent 4c242acf7b
commit 4422001c5e
3 changed files with 84 additions and 9 deletions
+41
View File
@@ -0,0 +1,41 @@
/**
* Panel-host parenting tests — the layering half of a real defect: on DSH
* Desktop's framed modes the shell tags the upstream `#root` with
* `data-dsh-desktop-content-viewport` and gives it `isolation: isolate`, a
* stacking context around the whole app INCLUDING the shell's overlay layer
* (`.dshDesktopOverlay`, z-index 1000 — where a plugin's modal renders, e.g.
* the community market's). A host parented to `document.body` sits outside
* that context, so the panel layer's documented 25 outranked every overlay
* inside it and covered host modals.
*
* jsdom: the resolution is a DOM query, so the stub-globals spec style used
* by desktop-env.spec.ts cannot exercise it.
*/
// @vitest-environment jsdom
import { beforeEach, describe, expect, it } from 'vitest'
import { panelHostParent } from '../src/client/desktop-env.ts'
beforeEach(() => { document.body.innerHTML = '' })
describe('panelHostParent', () => {
it('falls back to the body when the shell framed no content viewport', () => {
document.body.innerHTML = '<div id="root"></div>'
expect(panelHostParent(document)).toBe(document.body)
})
it('falls back to the body when there is no root at all (plain web)', () => {
expect(panelHostParent(document)).toBe(document.body)
})
it('resolves the framed content viewport when the shell stamped one', () => {
document.body.innerHTML = '<div id="root" data-dsh-desktop-content-viewport=""><div class="dshDesktopFrame"></div></div>'
const root = document.getElementById('root')
expect(panelHostParent(document)).toBe(root)
// The host lands INSIDE the shell's stacking context — beside the overlay
// layer the shell renders there, and never as a body-level sibling.
const host = document.createElement('div')
panelHostParent(document).appendChild(host)
expect(root?.contains(host)).toBe(true)
expect(document.body.lastElementChild).toBe(root)
})
})