fix(dashboard): settings drawer scrim covers viewport (host transform fix)

* fix(ci): wasm-pack PATH + Dockerfile workspace stub

Closes the two post-merge failures from #436:

1. wasm-pack: command not found — cargo install doesn't reliably leave
   the binary on PATH. Switched to the canonical installer in both the
   Pages and a11y workflows.
2. nvsim-server Docker build — cargo couldn't resolve workspace.dependencies
   from a partial copy. Dockerfile now generates a stub workspace
   Cargo.toml inline that lists just nvsim + nvsim-server.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(dashboard): settings drawer scrim — escape host transform's containing-block trap

The drawer's :host had transform: translateX(...) which makes it the
containing block for any fixed-position descendants. The .scrim at
'position: fixed; inset: 0' therefore covered only the drawer's own
420 px panel area, not the viewport. Visible symptoms:

- Page behind the drawer didn't dim
- Click outside the drawer didn't dismiss it (no scrim to receive)
- Felt like the drawer wasn't really 'modal'

Fix: keep :host as a fixed full-viewport overlay (no transform),
move the drawer body into an inner .panel div, transform only that.
Now the scrim covers the viewport correctly and outside-clicks dismiss.

Same trap exists nowhere else; nv-modal already follows this pattern.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
rUv
2026-04-27 13:59:34 -04:00
committed by GitHub
parent f02d9f0617
commit b123879b25
+23 -12
View File
@@ -9,26 +9,35 @@ export class NvSettingsDrawer extends LitElement {
@state() private open = false;
static styles = css`
/* The host covers the viewport without transforming itself. Only the
* inner .panel is transformed; otherwise the host's transform would
* create a containing block for the fixed-position scrim, clipping
* it to the panel's 420 px width and breaking outside-to-dismiss. */
:host {
position: fixed; top: 0; right: 0; bottom: 0;
position: fixed; inset: 0;
z-index: 51;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
}
:host([open]) { pointer-events: auto; opacity: 1; }
.scrim {
position: absolute; inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
}
.panel {
position: absolute;
top: 0; right: 0; bottom: 0;
width: 420px; max-width: 100vw;
background: var(--bg-1);
border-left: 1px solid var(--line);
z-index: 51;
transform: translateX(100%);
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
display: flex; flex-direction: column;
box-shadow: -20px 0 60px -20px rgba(0,0,0,0.5);
box-shadow: -20px 0 60px -20px rgba(0, 0, 0, 0.5);
}
:host([open]) { transform: translateX(0); }
.scrim {
position: fixed; inset: 0;
background: rgba(0,0,0,0.5);
z-index: 50;
opacity: 0; pointer-events: none;
transition: opacity 0.2s;
}
:host([open]) .scrim { opacity: 1; pointer-events: auto; }
:host([open]) .panel { transform: translateX(0); }
.h {
padding: 14px 16px;
border-bottom: 1px solid var(--line);
@@ -123,6 +132,7 @@ export class NvSettingsDrawer extends LitElement {
override render() {
return html`
<div class="scrim" @click=${() => this.close()}></div>
<div class="panel" role="dialog" aria-modal="true" aria-label="Settings">
<div class="h">
<div class="ttl">Settings</div>
<button class="close" @click=${() => this.close()}>×</button>
@@ -256,6 +266,7 @@ export class NvSettingsDrawer extends LitElement {
</div>
</div>
</div>
</div>
`;
}
}