The gallery pane was ported from a working one, two lines out of three

rega's census refutes the min-height diagnosis I gave, and the arithmetic is what
settles it. Inside section.dms-pane the CONTENT measures exactly the viewport: 900 in a
794px pane at 1440x900, 844 in a 712px pane at 390x844. The overflow is not "about the
DX-life header", it IS the header -- 106 and 132 px -- and it happens on 12 of 16 tabs
INCLUDING plugins that assert no floor of their own, which is what proves the floor is
not theirs.

library/ProfileLoader.vue:336 floors EVERY profile at
`min-height: var(--profile-min-h, 100dvh)`. That measures against the VIEWPORT while the
pane measures against its HOST, so wherever anything sits above the gallery the profile
is taller than the pane by exactly that chrome, and .dms-pane's overflow-y:auto scrolls
the whole plugin -- tab strip included. ProfileLoader documents the trap and documents
the only lever: the floor is inside the child's SHADOW ROOT, where no host-page selector
reaches, so an inherited custom property is all a host has. `--profile-min-h: 0`.

IT IS TWO THIRDS OF A FIX, NOT A NEW DEFECT. .vtl-host and plugin-studio's .ps-pane
already set it. .dms-pane was copied from .vtl-host -- its own comment says "porting the
same proven shape" -- and took the first two lines: fill the host, own the overflow. The
third, release the child's viewport floor, is the one that makes the other two safe.

So the gate now asserts the THREE AS ONE SHAPE: a rule that is absolutely-filling AND
scrolling must release the floor. It also FAILS if it cannot find such a rule at all,
because a gate that silently stops finding its subject is the failure I spent this
morning removing from check:i18n-keys-exist -- a check that matches nothing reports the
same green as a check that matches everything.

Negative-tested: removing the opt-out from either pane fails it, and changing the pane's
shape so the gate cannot recognise it fails it too rather than passing blind.

MY DIAGNOSIS WAS SECOND-ORDER AND I SHOULD SAY SO PLAINLY. The min-height floors were
real and worth fixing, but they only bite when a plugin's own floor exceeds the pane.
This bites on every tab at every size. I reasoned from source to the first mechanism
that explained the one symptom I had been shown, and the tall-desktop arm -- the one I
asked rega to run precisely because it would refute me -- is what separated them.
This commit is contained in:
Doxa Maps 2026-09-28 16:02:51 +00:00
parent 05ceeb2289
commit 00e505404c
2 changed files with 55 additions and 2 deletions

View file

@ -90,9 +90,50 @@ for (const bundle of readdirSync(DESIGNS)) {
}
}
// ── ★ THE HOST-SIZED SCROLLER IS A THREE-LINE SHAPE, AND IT MUST BE PORTED WHOLE ────────────────
// A pane that fills its host absolutely AND turns on overflow is also, necessarily, the thing that
// scrolls whatever its child overflows by. library/ProfileLoader.vue floors EVERY profile at
// `min-height: var(--profile-min-h, 100dvh)` -- a VIEWPORT measurement -- while such a pane measures
// against its HOST. So on any page with chrome above it, the child is taller than the pane by exactly
// the height of that chrome, and the pane scrolls the whole plugin, tab strip and all.
//
// That floor lives inside the child's SHADOW ROOT, where no host-page selector reaches. The single
// lever is the inherited custom property, which ProfileLoader documents as the opt-out. So the three
// belong together: fill the host, own the overflow, and release the child's viewport floor.
//
// ★ THIS IS GATED BECAUSE IT WAS ALREADY PORTED ONCE AND ARRIVED SHORT. doxa-maps-spa's .dms-pane was
// copied from VerticalTabsLayout's .vtl-host -- its own comment says "porting the same proven shape" --
// and took the first two lines. rega measured the result: 12 of 16 gallery tabs overflowing by exactly
// the DX-life header height, on plugins with no floor of their own. Two thirds of a fix reads as a
// whole one in review, which is precisely why a reviewer cannot be the check.
for (const [file, label] of [
['plugin-designs/doxa-maps-spa/doxa-maps-spa.vue', '.dms-pane'],
['library/layouts/VerticalTabsLayout.vue', '.vtl-host'],
]) {
const p = join(ROOT, file)
if (!existsSync(p)) { fails.push(`${file} is gone — this gate names it by path and now checks nothing`); continue }
const src = readFileSync(p, 'utf8')
// The rule for each pane: any rule that is BOTH absolutely-filling AND scrolling must release the floor.
const rules = src.match(/[^{}\n]*\{[^{}]*\}/g) || []
const scrollers = rules.filter((r) => /position:\s*absolute/.test(r) && /inset:\s*0/.test(r) && /overflow(-y)?:\s*auto/.test(r))
if (!scrollers.length) {
fails.push(`${file}: no absolutely-filling scroller found for ${label} — the shape moved and this gate is now blind to it`)
continue
}
for (const r of scrollers) {
if (!/--profile-min-h:\s*0/.test(r)) {
const sel = r.slice(0, r.indexOf('{')).trim().split('\n').pop().trim()
fails.push(`${file}: "${sel}" fills its host and owns the overflow but does NOT set --profile-min-h:0.\n`
+ ' ProfileLoader floors every child at 100dvh (the VIEWPORT), so on a page with a header the\n'
+ ' child is taller than this pane by the header height and this pane scrolls the whole plugin,\n'
+ ' tab strip included. The three lines are one shape: fill, scroll, release the floor.')
}
}
}
if (fails.length) {
console.error(`${TAG}: FAIL`)
for (const f of fails) console.error(` ✗ ${f}`)
process.exit(1)
}
console.log(`${TAG}: PASS — the gallery pins plugins with min-height:0, and no plugin asserts a floor inside its shadow root that a short pane could not honour.`)
console.log(`${TAG}: PASS — the gallery pins plugins with min-height:0, and no plugin asserts a floor inside its shadow root that a short pane could not honour, and every host-sized scroller releases ProfileLoader's viewport floor.`)

View file

@ -1468,7 +1468,19 @@ export default defineComponent({
viewport (the Big Picture dashboard+grid stack) gets a scrollbar, which is the one that needs it.
Do NOT go the other way and remove .dms-pane's absolute/inset — that was measured and produced a
worse defect (grid landing far below the fold); overflow:auto on a BOUNDED pane is correct. */
.dms-pane { position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; }
/* ★ --profile-min-h:0 IS THE THIRD LINE OF THE SHAPE ABOVE, AND IT IS THE ONE THAT WAS LEFT BEHIND.
library/ProfileLoader.vue:336 floors EVERY profile at `min-height: var(--profile-min-h, 100dvh)`.
That floor measures against the VIEWPORT while this pane measures against its HOST, so on any page
with chrome above the gallery the profile is taller than the pane by exactly the height of that
chrome -- and .dms-pane's overflow-y:auto then scrolls the whole plugin, tab strip included.
MEASURED by rega on DX Life: 1440x900 viewport, 794px pane, content exactly 900 (106px over, the
header); 390x844, 712px pane, content exactly 844 (132px). On 12 of 16 tabs, INCLUDING plugins with
no min-height of their own -- which is what proves the floor is ProfileLoader's, not theirs.
It cannot be fixed from outside: the floor is in the element's SHADOW ROOT where no host-page
selector reaches, so an inherited custom property is the only lever a host has. ProfileLoader
documents this opt-out for exactly this case, and .vtl-host and plugin-studio's .ps-pane both set
it. This pane was ported from .vtl-host and took two of its three lines. */
.dms-pane { position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; --profile-min-h: 0; }
.dms-slot { position: relative; min-height: 100%; }
.dms-hint {
position: absolute; inset: 0; display: grid; place-items: center; text-align: center;