Merge p15-w2-controls-fixes into dev
Phase 15 follow-up: fix seven control-panel + knob defects from Daniel's smoke test — greyer panel ground, drag scrollbar + body-scroll lock, light caption icons, centered WAVE slider, milder scrim, overlay above header/footer, and real RadialKnob pointer capture (site-wide stuck-knob fix).
This commit is contained in:
@@ -399,8 +399,9 @@ h2, h3, h4, h5, h6,
|
|||||||
section labels are LIGHT (static). The slider track/thumb and the lamp toggles are green.
|
section labels are LIGHT (static). The slider track/thumb and the lamp toggles are green.
|
||||||
============================================================================= */
|
============================================================================= */
|
||||||
.waveform-visualizer-control-panel.mix-visualizer-controls-bar {
|
.waveform-visualizer-control-panel.mix-visualizer-controls-bar {
|
||||||
/* Lighter-navy elevated panel ground (§5: navy-mid). */
|
/* Greyed panel ground — desaturated charcoal so the blue slider reads against it (defect #1).
|
||||||
background: var(--deepdrft-navy-mid);
|
Token is tunable in deepdrft-tokens.css without touching this rule. */
|
||||||
|
background: var(--deepdrft-panel-ground);
|
||||||
/* Square corners + thin light border — NowPlayingCard chrome (§5). */
|
/* Square corners + thin light border — NowPlayingCard chrome (§5). */
|
||||||
border: 1px solid var(--deepdrft-border-light);
|
border: 1px solid var(--deepdrft-border-light);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -424,11 +425,12 @@ h2, h3, h4, h5, h6,
|
|||||||
|
|
||||||
/* ── Row layout (§3). Each row is a horizontal band. Row 1 (MODE) and row 3 (WAVE) use
|
/* ── Row layout (§3). Each row is a horizontal band. Row 1 (MODE) and row 3 (WAVE) use
|
||||||
space-between so the right-pinned control (color / width) hugs the far edge. Row 2 (LAVA) uses
|
space-between so the right-pinned control (color / width) hugs the far edge. Row 2 (LAVA) uses
|
||||||
flex-start so its label + four knobs group left rather than spreading edge-to-edge. ── */
|
flex-start so its label + four knobs group left rather than spreading edge-to-edge.
|
||||||
|
align-items: center so the WAVE slider and section label vertically center with each other (defect #4). ── */
|
||||||
.waveform-visualizer-control-panel .wvc-row {
|
.waveform-visualizer-control-panel .wvc-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
gap: 0.85rem 1rem;
|
gap: 0.85rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +456,7 @@ h2, h3, h4, h5, h6,
|
|||||||
.waveform-visualizer-control-panel .wvc-row-left {
|
.waveform-visualizer-control-panel .wvc-row-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
gap: 0.85rem 1rem;
|
gap: 0.85rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,29 +498,52 @@ h2, h3, h4, h5, h6,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Caption icons + section labels render LIGHT (§5/§9 colour principle: static/decorative = light). MudIcon
|
/* Caption icons render LIGHT (§5/§9: static/decorative = light). !important beats the scoped
|
||||||
is portaled here too, so this is a plain global descendant selector — no ::deep, no scope attribute (CSS
|
.mix-visualizer-control ::deep .mix-visualizer-control-icon rule (which sets green for the legacy
|
||||||
isolation does not reach inside the overlay). The knob arcs/pointers + slider stay green (interactive). */
|
inline mount) when the icon also carries mix-visualizer-control-icon. Lamp toggles are MudIconButton
|
||||||
|
not MudIcon so they are unaffected — they stay green (interactive, Color.Primary). (defect #3) */
|
||||||
.waveform-visualizer-control-panel .waveform-visualizer-control-icon {
|
.waveform-visualizer-control-panel .waveform-visualizer-control-icon {
|
||||||
color: var(--deepdrft-white);
|
color: var(--deepdrft-white) !important;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── The modal overlay (Phase 15 §4). MudOverlay is already a full-viewport flex scrim that centers its
|
/* ── The modal overlay (Phase 15 §4). MudOverlay is already a full-viewport flex scrim that centers its
|
||||||
content (.mud-overlay { display:flex; align-items:center; justify-content:center }), which gives the
|
content (.mud-overlay { display:flex; align-items:center; justify-content:center }), which gives the
|
||||||
screen-centered panel on every host for free — we do NOT fight that positioning. We only (a) set the
|
screen-centered panel on every host for free — we do NOT fight that positioning. We:
|
||||||
mild modal tint from the SINGLE --deepdrft-modal-scrim-alpha token (§10.5, one point of change) and
|
(a) Raise the overlay z-index above the header (100) and the player-dock footer (1200/1300) so the
|
||||||
(b) cap the centered content's height so a tall both-on deck scrolls inside the modal rather than
|
scrim tints the ENTIRE viewport uniformly — header and footer included (defect #7). The panel
|
||||||
overflowing the viewport. The overlay portals to the body, so these are plain global rules (no scope
|
content needs z-index: auto (inherits from stacking context) so it sits above the scrim naturally;
|
||||||
attribute). The doubled .mud-overlay-scrim.mud-overlay-dark selector (0,2,0) outranks MudBlazor's own
|
the RadialKnob capture div at 9999 remains above everything.
|
||||||
.mud-overlay-dark (0,1,0), so the tint wins regardless of stylesheet load order. ── */
|
(b) Set the mild tint from the SINGLE --deepdrft-modal-scrim-alpha token (§10.5, defect #6).
|
||||||
|
(c) Remove overflow-y:auto on the content wrapper — it was the source of the drag scrollbar (defect #2).
|
||||||
|
The panel's max-width/flex-column already contain its size; the outer overlay clips at 100vh.
|
||||||
|
(d) Suppress body scroll while the overlay is present so no page-scroll occurs during a drag (defect #2).
|
||||||
|
The overlay portals to the body, so these are plain global rules (no scope attribute). The doubled
|
||||||
|
.mud-overlay-scrim.mud-overlay-dark selector (0,2,0) outranks MudBlazor's own .mud-overlay-dark (0,1,0),
|
||||||
|
so the tint wins regardless of stylesheet load order. ── */
|
||||||
|
|
||||||
|
/* Raise the overlay itself above the sticky header (z-index:100) and the fixed player dock (z-index:1200).
|
||||||
|
Use 1400 so it sits above the minimized-dock FAB (1300) too. The panel content inherits this context
|
||||||
|
and stacks above the scrim; the RadialKnob capture div (z-index:9999) stays highest. */
|
||||||
|
.waveform-visualizer-control-overlay {
|
||||||
|
z-index: 1400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.waveform-visualizer-control-overlay .mud-overlay-scrim.mud-overlay-dark {
|
.waveform-visualizer-control-overlay .mud-overlay-scrim.mud-overlay-dark {
|
||||||
background-color: rgba(var(--deepdrft-scrim-rgb), var(--deepdrft-modal-scrim-alpha));
|
background-color: rgba(var(--deepdrft-scrim-rgb), var(--deepdrft-modal-scrim-alpha));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* No overflow-y:auto — removing it eliminates the spurious scrollbar that appeared while dragging a
|
||||||
|
knob (defect #2). The panel's flex-column layout is self-contained and never overflows the overlay. */
|
||||||
.waveform-visualizer-control-overlay .mud-overlay-content {
|
.waveform-visualizer-control-overlay .mud-overlay-content {
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lock body scroll while the controls overlay is open so the page cannot be scrolled during a
|
||||||
|
knob drag (defect #2). :has() degrades gracefully in older browsers (no lock, no crash). */
|
||||||
|
body:has(.waveform-visualizer-control-overlay) {
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 419.98px) {
|
@media (max-width: 419.98px) {
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
<!-- Global mouse capture container when dragging -->
|
<!-- Drag-shield: full-viewport overlay while dragging.
|
||||||
|
position:fixed; inset:0; z-index:9999 so it sits above all overlays (the controls modal is z-index:1400).
|
||||||
|
Provides the cursor hint and blocks stray clicks on underlying content during a drag.
|
||||||
|
Event delivery is NOT via this div — setPointerCapture on the knob element routes
|
||||||
|
pointermove/pointerup/pointercancel directly to the knob regardless of where the cursor is,
|
||||||
|
even outside the browser window. This overlay is a belt-and-suspenders UX guard only. -->
|
||||||
@if (_isDragging)
|
@if (_isDragging)
|
||||||
{
|
{
|
||||||
<div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 9999; cursor: ns-resize;"
|
<div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 9999; cursor: ns-resize;">
|
||||||
@onmousemove="@OnGlobalMouseMove" @onmouseup="@OnGlobalMouseUp">
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Knob element owns pointer capture during drag.
|
||||||
|
@onpointerdown initiates drag and calls setPointerCapture via knob.js.
|
||||||
|
@onpointermove / @onpointerup / @onpointercancel are delivered here (not to the overlay)
|
||||||
|
for the captured pointer, even when the cursor has left the browser window. -->
|
||||||
<div class="radial-knob" style="width: @(Size)px; height: @(Size)px; display: inline-block; position: relative;"
|
<div class="radial-knob" style="width: @(Size)px; height: @(Size)px; display: inline-block; position: relative;"
|
||||||
@onmousedown="@OnMouseDown">
|
@ref="_knobRef"
|
||||||
|
@onpointerdown="@OnPointerDown"
|
||||||
|
@onpointermove="@OnPointerMove"
|
||||||
|
@onpointerup="@OnPointerUp"
|
||||||
|
@onpointercancel="@OnPointerCancel">
|
||||||
|
|
||||||
<!-- SVG Knob -->
|
<!-- SVG Knob -->
|
||||||
<svg width="@Size" height="@Size" viewBox="0 0 100 100">
|
<svg width="@Size" height="@Size" viewBox="0 0 100 100">
|
||||||
@@ -78,6 +92,9 @@
|
|||||||
[Parameter] public MudBlazor.Color Color { get; set; } = MudBlazor.Color.Primary;
|
[Parameter] public MudBlazor.Color Color { get; set; } = MudBlazor.Color.Primary;
|
||||||
[Parameter] public bool HoldValue { get; set; } = false;
|
[Parameter] public bool HoldValue { get; set; } = false;
|
||||||
|
|
||||||
|
private ElementReference _knobRef;
|
||||||
|
private IJSObjectReference? _knobModule;
|
||||||
|
|
||||||
private bool _isDragging = false;
|
private bool _isDragging = false;
|
||||||
private double _lastMouseY = 0;
|
private double _lastMouseY = 0;
|
||||||
private double _dragValue = 0;
|
private double _dragValue = 0;
|
||||||
@@ -150,25 +167,36 @@
|
|||||||
return y.ToString("F1", CultureInfo.InvariantCulture);
|
return y.ToString("F1", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnMouseDown(MouseEventArgs e)
|
private async Task OnPointerDown(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
_isDragging = true;
|
_isDragging = true;
|
||||||
_lastMouseY = e.ClientY;
|
_lastMouseY = e.ClientY;
|
||||||
_dragValue = Value; // Initialize drag value with current value
|
_dragValue = Value; // Initialize drag value with current value
|
||||||
|
|
||||||
// Add global mouse event handlers using Blazor's event handling
|
// Load the JS module on first drag (lazy — avoids import cost until the user interacts).
|
||||||
|
_knobModule ??= await JS.InvokeAsync<IJSObjectReference>(
|
||||||
|
"import", "./_content/DeepDrftShared.Client/js/knob/knob.js");
|
||||||
|
|
||||||
|
// Capture the pointer on the knob element so pointermove/pointerup are delivered
|
||||||
|
// even when the cursor leaves the browser window mid-drag.
|
||||||
|
await _knobModule.InvokeVoidAsync("capturePointer", _knobRef, e.PointerId);
|
||||||
|
|
||||||
|
// The full-viewport capture div (rendered when _isDragging) is a belt-and-suspenders
|
||||||
|
// guard that blocks stray clicks on underlying content while dragging.
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnGlobalMouseMove(MouseEventArgs e)
|
// Delivered to the knob element because setPointerCapture routes the captured pointer here.
|
||||||
|
private async Task OnPointerMove(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
if (_isDragging)
|
if (_isDragging)
|
||||||
{
|
{
|
||||||
await UpdateValueFromMouse(e);
|
await UpdateValueFromPointer(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnGlobalMouseUp(MouseEventArgs e)
|
// pointerup implicitly releases pointer capture — no explicit releasePointerCapture needed.
|
||||||
|
private async Task OnPointerUp(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
if (_isDragging && HoldValue)
|
if (_isDragging && HoldValue)
|
||||||
{
|
{
|
||||||
@@ -183,9 +211,35 @@
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateValueFromMouse(MouseEventArgs e)
|
// Pointer capture cancelled by OS (e.g. Alt+Tab, system gesture) — end drag cleanly.
|
||||||
|
// Delivered to the knob element (the capturing element). Release capture explicitly since
|
||||||
|
// the implicit release on pointerup does not apply to pointercancel.
|
||||||
|
private async Task OnPointerCancel(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
// Calculate vertical delta from last mouse position
|
if (_knobModule != null)
|
||||||
|
{
|
||||||
|
try { await _knobModule.InvokeVoidAsync("releasePointer", _knobRef, e.PointerId); }
|
||||||
|
catch (JSException) { /* element may already be gone */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDragging = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_knobModule != null)
|
||||||
|
{
|
||||||
|
try { await _knobModule.DisposeAsync(); }
|
||||||
|
catch (JSDisconnectedException) { /* circuit torn down */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateValueFromPointer(PointerEventArgs e)
|
||||||
|
{
|
||||||
|
// Calculate vertical delta from last pointer position
|
||||||
double deltaY = _lastMouseY - e.ClientY; // Inverted: up = positive, down = negative
|
double deltaY = _lastMouseY - e.ClientY; // Inverted: up = positive, down = negative
|
||||||
_lastMouseY = e.ClientY;
|
_lastMouseY = e.ClientY;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* knob - pointer capture helpers for RadialKnob.
|
||||||
|
*
|
||||||
|
* setPointerCapture / releasePointerCapture are not exposed via Blazor's
|
||||||
|
* ElementReference, so the component delegates here via JS interop.
|
||||||
|
* Both functions are no-ops when the element reference is stale (e.g. the
|
||||||
|
* component was disposed between the JS call and the microtask).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Capture the pointer on the given element so pointermove/pointerup are
|
||||||
|
* delivered even when the cursor leaves the browser window. */
|
||||||
|
export function capturePointer(el: Element, pointerId: number): void {
|
||||||
|
(el as HTMLElement).setPointerCapture(pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release a previously captured pointer. Called on pointercancel.
|
||||||
|
* pointerup releases capture implicitly, but we call this on cancel too. */
|
||||||
|
export function releasePointer(el: Element, pointerId: number): void {
|
||||||
|
(el as HTMLElement).releasePointerCapture(pointerId);
|
||||||
|
}
|
||||||
@@ -26,7 +26,10 @@
|
|||||||
--deepdrft-scrim-rgb: 13, 27, 42;
|
--deepdrft-scrim-rgb: 13, 27, 42;
|
||||||
/* Modal scrim opacity — the SINGLE point of truth for the visualizer-controls overlay tint
|
/* Modal scrim opacity — the SINGLE point of truth for the visualizer-controls overlay tint
|
||||||
(Phase 15 §4/§10.5). Mild so the panel reads as modal without a blackout. Change here once. */
|
(Phase 15 §4/§10.5). Mild so the panel reads as modal without a blackout. Change here once. */
|
||||||
--deepdrft-modal-scrim-alpha: 0.3;
|
--deepdrft-modal-scrim-alpha: 0.15;
|
||||||
|
/* Panel ground — desaturated from navy-mid toward charcoal so the blue slider stands out.
|
||||||
|
Tunable: increase blue channel (e.g. #1e2235) to recover warmth, lower (e.g. #1a1d22) to go darker. */
|
||||||
|
--deepdrft-panel-ground: #1e2028;
|
||||||
|
|
||||||
/* Wireframe font stack */
|
/* Wireframe font stack */
|
||||||
--deepdrft-font-display: "Cormorant Garamond", Georgia, serif;
|
--deepdrft-font-display: "Cormorant Garamond", Georgia, serif;
|
||||||
|
|||||||
Reference in New Issue
Block a user