Dashboard options: windows.*, detent names, positive number types

- dashboard.windowMaxScale / windowDistance -> dashboard.windows.maxScale /
  distance
- windowCurvature: default -> initial, snapPixels/snapPoints ->
  detentPixels/detentPoints, dragThreshold -> dragThresholdPixels (also the
  patch's option keys; window-curvature VERSION 19); removed `snap` now
  points to detentPixels
- types.numbers.positive/nonnegative instead of number + range assertions
  (step <= max, initial/detentPoints <= max and distance min <= max stay)
- frame-controls patch accepts longPressMs from 300, like the option
  (VERSION 2)
- "SteamVR dashboard patch" in descriptions, which fit into 80 columns

Old names keep working with a rename warning.
This commit is contained in:
Pierre Kisters committed 2026-09-28 03:36:38 +02:00
1 parent a0c3e0846f
commit 71c78bd733
8 files changed
+121 -100

No files matched your search

+35 -32
View File
@@ -1,10 +1,10 @@
# Resize and grab-distance limits of SteamVR dashboard windows. Dashboard patch
# (dashboard-windows/patch.js; "systemui" on 127.0.0.1:8087), registered only
# when an option is set; enables steamvr-debugger.nix (one SteamVR restart the
# first time). Unsetting all options reverts to stock.
# Resize and grab-distance limits of SteamVR dashboard windows. SteamVR
# dashboard patch (dashboard-windows/patch.js; "systemui" on 127.0.0.1:8087),
# registered only when an option is set; enables steamvr-debugger.nix (one
# SteamVR restart the first time). Unsetting all options reverts to stock.
{ config, lib, pkgs, ... }:
let
cfg = config.steamFrame.dashboard;
cfg = config.steamFrame.dashboard.windows;
inherit (lib) mkOption types;
inherit (import ./lib { inherit pkgs; }) mkPatch;
@@ -17,7 +17,7 @@ let
rangeOption = kind: what: {
min = mkOption {
type = types.nullOr types.number;
type = types.nullOr types.numbers.positive;
default = null;
description = ''
Closest distance (m) ${what} can be pulled in to while grabbed; null =
@@ -25,7 +25,7 @@ let
'';
};
max = mkOption {
type = types.nullOr types.number;
type = types.nullOr types.numbers.positive;
default = null;
example = stockDistance.${kind}.max * 2;
description = ''
@@ -36,43 +36,49 @@ let
};
effective = kind: {
min = if cfg.windowDistance.${kind}.min != null then cfg.windowDistance.${kind}.min else stockDistance.${kind}.min;
max = if cfg.windowDistance.${kind}.max != null then cfg.windowDistance.${kind}.max else stockDistance.${kind}.max;
min = if cfg.distance.${kind}.min != null then cfg.distance.${kind}.min else stockDistance.${kind}.min;
max = if cfg.distance.${kind}.max != null then cfg.distance.${kind}.max else stockDistance.${kind}.max;
};
distanceSet = lib.any (r: r.min != null || r.max != null) (lib.attrValues cfg.windowDistance);
enabled = cfg.windowMaxScale != null || distanceSet;
in {
imports = [ ./steam-ui-patches.nix ./steamvr-debugger.nix ];
distanceSet = lib.any (r: r.min != null || r.max != null) (lib.attrValues cfg.distance);
enabled = cfg.maxScale != null || distanceSet;
options.steamFrame.dashboard = {
windowMaxScale = mkOption {
type = types.nullOr types.number;
old = [ "steamFrame" "dashboard" ];
new = [ "steamFrame" "dashboard" "windows" ];
in {
imports = [
./steam-ui-patches.nix ./steamvr-debugger.nix
(lib.mkRenamedOptionModule (old ++ [ "windowMaxScale" ]) (new ++ [ "maxScale" ]))
] ++ lib.concatMap (kind: map (bound:
lib.mkRenamedOptionModule
(old ++ [ "windowDistance" kind bound ]) (new ++ [ "distance" kind bound ]))
[ "min" "max" ]) (lib.attrNames stockDistance);
options.steamFrame.dashboard.windows = {
maxScale = mkOption {
type = types.nullOr types.numbers.positive;
default = null;
example = 4.0;
description = ''
Largest resize factor of SteamVR dashboard windows, relative to their
default size; null = stock (2). The theater screen starts 2.8x larger,
so its limit is 2.8x this. Dashboard patch, applied immediately.
so its limit is 2.8x this. SteamVR dashboard patch, applied
immediately.
'';
};
windowDistance = {
world = rangeOption "world" "windows placed in the world (not attached to the dashboard)";
distance = {
world = rangeOption "world"
"windows placed in the world (not attached to the dashboard)";
theater = rangeOption "theater" "the theater-mode screen";
dashboard = rangeOption "dashboard" "the dashboard itself";
};
};
config = lib.mkIf enabled {
assertions =
[ {
assertion = cfg.windowMaxScale == null || cfg.windowMaxScale > 0;
message = "steamFrame.dashboard.windowMaxScale must be positive.";
} ]
++ map (kind: let r = effective kind; in {
assertion = r.min > 0 && r.max >= r.min;
message = "steamFrame.dashboard.windowDistance.${kind}: need 0 < min <= max (stock values fill in unset ones), got ${toString r.min}-${toString r.max}.";
}) (lib.attrNames stockDistance);
assertions = map (kind: let r = effective kind; in {
assertion = r.max >= r.min;
message = "steamFrame.dashboard.windows.distance.${kind}: need min <= max (stock values fill in unset ones), got ${toString r.min}-${toString r.max}.";
}) (lib.attrNames stockDistance);
steamFrame.uiPatches.patches = [ {
name = "dashboard-windows";
@@ -81,10 +87,7 @@ in {
patch = mkPatch {
name = "dashboard-windows";
src = ./dashboard-windows/patch.js;
opts = {
maxScale = cfg.windowMaxScale;
distance = cfg.windowDistance;
};
opts = { inherit (cfg) maxScale distance; };
};
unpatch = ./dashboard-windows/unpatch.js;
} ];
+2 -1
View File
@@ -34,7 +34,8 @@ let
in {
options.steamFrame.firefox = {
enable = lib.mkEnableOption ''
the Firefox Flatpak (org.mozilla.firefox) launcher with Steam Frame fixes'';
the Firefox Flatpak (org.mozilla.firefox) launcher with Steam Frame
fixes'';
vrFullscreenFix = lib.mkOption {
type = lib.types.bool;
default = true;
+5 -4
View File
@@ -1,7 +1,8 @@
# Move window controls between a dashboard window's bottom bar and its
# three-dot menu (long press). Dashboard patch (frame-controls/patch.js;
# "systemui" on 127.0.0.1:8087), registered only when enabled; enables
# steamvr-debugger.nix (one SteamVR restart the first time).
# three-dot menu (long press). SteamVR dashboard patch
# (frame-controls/patch.js; "systemui" on 127.0.0.1:8087), registered only
# when enabled; enables steamvr-debugger.nix (one SteamVR restart the first
# time).
{ config, lib, pkgs, ... }:
let
cfg = config.steamFrame.dashboard.frameControls;
@@ -25,7 +26,7 @@ in {
moving SteamVR dashboard window controls between the bottom bar and the
three-dot menu: long press an icon or menu row for a "Show in bar"
popup. Applies to that control in all windows, kept across SteamVR
restarts. Dashboard patch; off restores stock (next switch)'';
restarts. SteamVR dashboard patch; off restores stock (next switch)'';
longPressMs = mkOption {
type = types.ints.between 300 10000;
default = 1500;
+2 -2
View File
@@ -59,7 +59,7 @@
// setPlacement(name or "icon:N", 'bar' | 'menu' | null), reset().
((find, sigs, opts) => {
const NAME = 'frame-controls';
const VERSION = 1;
const VERSION = 2;
const T_SPACER = 1, T_ACTION = 2;
const LS_KEY = 'sfui.frameControls.v1';
// Action icon enums (sigs.controls anchors them): names for the options.
@@ -70,7 +70,7 @@
const TOOLTIP_OFFSET = '0 0.15 0.06'; // stock tooltip / menu offset (y 0.15, z 0.05), in front
const o = { inBar: [], inMenu: [], longPressMs: 1500, floatInTheater: false, ...opts };
if (!(o.longPressMs >= 200)) return `invalid options ${JSON.stringify(opts)}`;
if (!(o.longPressMs >= 300)) return `invalid options ${JSON.stringify(opts)}`;
const toKey = (x) => (typeof x === 'number' ? `icon:${x}` : NAMES[x] != null ? `icon:${NAMES[x]}` : /^icon:\d+$/.test(x) ? x : null);
const optMap = {}; // key -> 'bar' | 'menu' from the options
for (const [list, where] of [[o.inBar, 'bar'], [o.inMenu, 'menu']]) {
+2 -2
View File
@@ -1,4 +1,4 @@
# Close (X) button on the dashboard's Steam window. Dashboard patch
# Close (X) button on the dashboard's Steam window. SteamVR dashboard patch
# (steam-close-button/patch.js; "systemui" on 127.0.0.1:8087), registered only
# when enabled; enables steamvr-debugger.nix (one SteamVR restart the first time).
{ config, lib, pkgs, ... }:
@@ -11,7 +11,7 @@ in {
options.steamFrame.dashboard.steamCloseButton.enable = lib.mkEnableOption ''
a Close (X) button on the dashboard's Steam window. It hides Steam
(previous window or just the bar) until shown explicitly, e.g. via the
Steam tab. Dashboard patch; off removes it (next switch)'';
Steam tab. SteamVR dashboard patch; off removes it (next switch)'';
config = lib.mkIf cfg.enable {
steamFrame.uiPatches.patches = [ {
+6 -3
View File
@@ -58,11 +58,14 @@ in {
options.steamFrame.steamvrDebugger.enable = lib.mkOption {
type = lib.types.bool;
default = false;
defaultText = lib.literalMD "on automatically when a dashboard patch (a `steamFrame.uiPatches.patches` entry on port 8087) is enabled";
defaultText = lib.literalMD ''
on automatically when a SteamVR dashboard patch (a
`steamFrame.uiPatches.patches` entry on port 8087) is enabled
'';
description = ''
SteamVR's web helper debugger (dashboard DevTools on 127.0.0.1:8087,
VRWebHelper/DebuggerEnabled in steamvr.vrsettings), needed by dashboard
patches, which turn it on automatically. Takes effect after one SteamVR
VRWebHelper/DebuggerEnabled in steamvr.vrsettings), needed by SteamVR
dashboard patches, which turn it on automatically. Takes effect after one SteamVR
restart; off resets the key at the next start. Developer Mode also
forwards the port to the LAN (0.0.0.0:8088).
'';
+46 -33
View File
@@ -1,4 +1,4 @@
# Adjustable curvature per dashboard window. Dashboard patch
# Adjustable curvature per dashboard window. SteamVR dashboard patch
# (window-curvature/patch.js; "systemui" on 127.0.0.1:8087), registered only
# when enabled; enables steamvr-debugger.nix (one SteamVR restart the first time).
{ config, lib, pkgs, ... }:
@@ -7,17 +7,23 @@ let
inherit (lib) mkOption types;
inherit (import ./lib { inherit pkgs; }) mkPatch;
value = default: description: mkOption {
type = types.number;
inherit default description;
path = [ "steamFrame" "dashboard" "windowCurvature" ];
rename = from: to: lib.mkRenamedOptionModule (path ++ [ from ]) (path ++ [ to ]);
number = type: default: description: mkOption {
inherit type default description;
};
in {
imports = [
./steam-ui-patches.nix ./steamvr-debugger.nix
(lib.mkRemovedOptionModule [ "steamFrame" "dashboard" "windowCurvature" "barDragRoom" ]
(lib.mkRemovedOptionModule (path ++ [ "barDragRoom" ])
"Not needed: SteamVR keeps sending the bar's coordinates past its edge during a drag.")
(lib.mkRemovedOptionModule [ "steamFrame" "dashboard" "windowCurvature" "snap" ]
"Snapping is a detent in drag pixels now: use steamFrame.dashboard.windowCurvature.snapPixels.")
(lib.mkRemovedOptionModule (path ++ [ "snap" ])
"Snapping is a detent in drag pixels now: use steamFrame.dashboard.windowCurvature.detentPixels.")
(rename "default" "initial")
(rename "snapPixels" "detentPixels")
(rename "snapPoints" "detentPoints")
(rename "dragThreshold" "dragThresholdPixels")
];
options.steamFrame.dashboard.windowCurvature = {
@@ -26,48 +32,56 @@ in {
Curvature" menu row (shows the value) or bar button: click toggles
curved/flat, drag up/down sets it live. Values are relative to the stock
curve (1 = stock, 2 = half the radius, 0 = flat), kept per window until
SteamVR restarts. Dashboard patch; off restores stock (next switch)'';
default = value 1.0 ''
SteamVR restarts. SteamVR dashboard patch; off restores stock (next
switch)'';
initial = number types.numbers.nonnegative 1.0 ''
Curvature a world/hand window (stock: flat) gets when first curved.
Dashboard and theater windows start at 1, keeping the docked Steam
window concentric with the bar.
'';
max = value 3.0 "Largest curvature the control goes to (relative to the stock curve).";
step = value 0.05 "Step the value is rounded to while dragging.";
snapPixels = mkOption {
max = number types.numbers.positive 3.0 ''
Largest curvature the control goes to (relative to the stock curve).
'';
step = number types.numbers.positive 0.05 ''
Step the value is rounded to while dragging.
'';
detentPixels = mkOption {
type = types.ints.unsigned;
default = 24;
description = ''
Detent at each snap point, in pixels of drag: the value holds at the
point for this much travel, then continues from it (no values are
skipped). 0 = no detent.
Width of the detent at each of `detentPoints`, in pixels of drag:
the value holds at the point for this much travel, then continues
from it (no values are skipped). 0 = no detent.
'';
};
snapPoints = mkOption {
type = types.listOf types.number;
detentPoints = mkOption {
type = types.listOf types.numbers.nonnegative;
default = [ 0 1.0 ];
description = "Values with a detent while dragging (0 = flat, 1 = stock curve).";
description = ''
Values with a detent while dragging (0 = flat, 1 = stock curve).
'';
};
dragThreshold = mkOption {
dragThresholdPixels = mkOption {
type = types.ints.unsigned;
default = 8;
description = ''
Vertical laser travel (menu px) that turns a press into a drag.
'';
};
dragPixelsPerUnit = value 120 ''
dragPixelsPerUnit = number types.numbers.positive 120 ''
Menu pixels per 1.0 of curvature (6 px per 0.05 step). The laser stops
at the menu's edge (~190 px above the row): with the default, 0 to 1
fits into one drag, the full range to `max` = 3 takes two.
'';
barDragPixelsPerUnit = value 60 ''
barDragPixelsPerUnit = number types.numbers.positive 60 ''
Bar pixels per 1.0 of curvature on the bottom-bar button.
'';
haptics = mkOption {
type = types.bool;
default = true;
description = ''
Controller haptics while dragging (steps, detents, 0/`max` edges); the dashboard's own hover clicks are muted during a drag.
Controller haptics while dragging (steps, detents, 0/`max` edges);
the dashboard's own hover clicks are muted during a drag.
'';
};
};
@@ -75,20 +89,16 @@ in {
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.max > 0 && cfg.step > 0 && cfg.step <= cfg.max;
message = "steamFrame.dashboard.windowCurvature: need max > 0 and 0 < step <= max, got max ${builtins.toJSON cfg.max}, step ${builtins.toJSON cfg.step}.";
assertion = cfg.step <= cfg.max;
message = "steamFrame.dashboard.windowCurvature: need step <= max, got max ${builtins.toJSON cfg.max}, step ${builtins.toJSON cfg.step}.";
}
{
assertion = cfg.default >= 0 && cfg.default <= cfg.max;
message = "steamFrame.dashboard.windowCurvature.default must be within 0 .. max (${builtins.toJSON cfg.max}), got ${builtins.toJSON cfg.default}.";
assertion = cfg.initial <= cfg.max;
message = "steamFrame.dashboard.windowCurvature.initial must be within 0 .. max (${builtins.toJSON cfg.max}), got ${builtins.toJSON cfg.initial}.";
}
{
assertion = lib.all (p: p >= 0 && p <= cfg.max) cfg.snapPoints;
message = "steamFrame.dashboard.windowCurvature: snapPoints must be within 0 .. max (${builtins.toJSON cfg.max}).";
}
{
assertion = cfg.dragPixelsPerUnit > 0 && cfg.barDragPixelsPerUnit > 0;
message = "steamFrame.dashboard.windowCurvature: dragPixelsPerUnit and barDragPixelsPerUnit must be positive.";
assertion = lib.all (p: p <= cfg.max) cfg.detentPoints;
message = "steamFrame.dashboard.windowCurvature: detentPoints must be within 0 .. max (${builtins.toJSON cfg.max}).";
}
];
@@ -99,7 +109,10 @@ in {
patch = mkPatch {
name = "window-curvature";
src = ./window-curvature/patch.js;
opts = removeAttrs cfg [ "enable" "snap" "barDragRoom" ];
opts = {
inherit (cfg) initial max step detentPixels detentPoints
dragThresholdPixels dragPixelsPerUnit barDragPixelsPerUnit haptics;
};
};
unpatch = ./window-curvature/unpatch.js;
} ];
+23 -23
View File
@@ -6,8 +6,8 @@
// curve); dragging up/down with the laser sets the curvature live.
//
// Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title
// "systemui"). mkPatch patch (see lib/default.nix); opts: { default, max,
// step, snapPixels, snapPoints, dragThreshold, dragPixelsPerUnit,
// "systemui"). mkPatch patch (see lib/default.nix); opts: { initial, max,
// step, detentPixels, detentPoints, dragThresholdPixels, dragPixelsPerUnit,
// barDragPixelsPerUnit, haptics }.
//
// Stock curvature (frame.curvature, systemui's `curvature` component):
@@ -26,7 +26,7 @@
// Values per window (key: first overlay key) in
// window.__sfuiWindowCurvatureState (schema 1: { values, log }); kept across
// re-patch and unpatch, not across a dashboard reload. Without a stored value:
// opts.default in the world or on a hand, 1 when docked in the dashboard or
// opts.initial in the world or on a hand, 1 when docked in the dashboard or
// theater (so the Steam window stays concentric with the dashboard bar).
//
// Bar button: the bar panel (#legacy-frame-controls-<frameID>) is one button
@@ -36,18 +36,18 @@
// origin reach the compositor at different times).
//
// Dragging: value = start value + drag distance / px per unit, rounded to
// step. Snap points are detents in drag distance: the value holds there for
// opts.snapPixels of travel, then continues, so no value is skipped.
// step. Detent points hold in drag distance: the value stays there for
// opts.detentPixels of travel, then continues, so no value is skipped.
//
// Haptics (opts.haptics): VROverlay.TriggerOverlayHapticEffect with stock
// EOverlayHapticEffect values: SlidingEdge at 0/max, Snap at snap points,
// EOverlayHapticEffect values: SlidingEdge at 0/max, Snap at detent points,
// Sliding per step (at most every 30 ms). During a drag the dashboard's own
// haptics (hover clicks on buttons the laser passes) are muted.
//
// Contract with other patches (e.g. frame-controls' long press); this patch
// owns press-and-drag on its controls:
// - Every element it drives has the class `sfui-curv-ctl`.
// - When a press becomes a drag (opts.dragThreshold px vertical) it
// - When a press becomes a drag (opts.dragThresholdPixels px vertical) it
// dispatches a bubbling CustomEvent `sfui-curv-dragstart` on the element
// (detail { frameID, where: 'menu' | 'bar' }); when that drag ends
// (release or cancelPress), `sfui-curv-dragend`.
@@ -64,18 +64,18 @@
// dragValue(v0, dy, ppu, cur).
((find, sigs, opts, hooks) => {
const NAME = 'window-curvature';
const VERSION = 18;
const VERSION = 19;
const FLAT = 999; // origin distances >= this are "flat"
const ICON_OFF = 40, ICON_ON = 39; // Toggle Curvature action icons (sigs.curvatureAction)
const HAPTIC = { Snap: 3, Sliding: 4, SlidingEdge: 5 }; // EOverlayHapticEffect (sigs.hapticEffects)
const o = {
default: 1, max: 3, step: 0.05, snapPixels: 24, snapPoints: [0, 1], dragThreshold: 8, dragPixelsPerUnit: 120,
initial: 1, max: 3, step: 0.05, detentPixels: 24, detentPoints: [0, 1], dragThresholdPixels: 8, dragPixelsPerUnit: 120,
barDragPixelsPerUnit: 60, haptics: true, ...opts,
};
if (!(o.max > 0 && o.step > 0 && o.step <= o.max && o.default >= 0 && o.default <= o.max && o.snapPixels >= 0 &&
Array.isArray(o.snapPoints) && o.snapPoints.every((p) => p >= 0 && p <= o.max) &&
o.dragThreshold >= 0 && o.dragPixelsPerUnit > 0 && o.barDragPixelsPerUnit > 0 &&
if (!(o.max > 0 && o.step > 0 && o.step <= o.max && o.initial >= 0 && o.initial <= o.max && o.detentPixels >= 0 &&
Array.isArray(o.detentPoints) && o.detentPoints.every((p) => p >= 0 && p <= o.max) &&
o.dragThresholdPixels >= 0 && o.dragPixelsPerUnit > 0 && o.barDragPixelsPerUnit > 0 &&
typeof o.haptics === 'boolean'))
return `invalid options ${JSON.stringify(opts)}`;
const id = JSON.stringify([VERSION, o]);
@@ -120,26 +120,26 @@
const keyOf = (f) => f.associatedSummonOverlayKeys?.[0] ?? `frame:${f.frameID}`;
const dockOf = (f) => f.docking?.visualDockLocation ?? f.docking?.dockLocation;
const strength = (f) => S.values[keyOf(f)] ??
([DOCK.LeftHand, DOCK.RightHand, DOCK.World].includes(dockOf(f)) ? o.default : 1);
([DOCK.LeftHand, DOCK.RightHand, DOCK.World].includes(dockOf(f)) ? o.initial : 1);
const curved = (f) => !!curvatureOf(f)?.shouldCurve;
const shown = (f) => (curved(f) ? strength(f) : 0);
// Value for a drag of dy px (up = positive) from v0 at ppu px per unit, or
// null to keep cur. Snap points are detents in drag distance: reaching one
// (or starting on it) holds the value there for snapPixels of travel, then
// it continues from the point, so no values are skipped. Off a snap point
// null to keep cur. Detent points hold in drag distance: reaching one
// (or starting on it) holds the value there for detentPixels of travel,
// then it continues from the point, so no values are skipped. Off a point
// the value is rounded to step; a change of less than 0.6 steps away from
// cur is ignored (laser jitter at a step boundary).
const dragValue = (v0, dy, ppu, cur) => {
const dir = Math.sign(dy);
let v = v0, rest = Math.abs(dy), held = null;
const points = o.snapPoints.filter((p) => (dir > 0 ? p >= v0 : p <= v0)).sort((a, b) => dir * (a - b));
const points = o.detentPoints.filter((p) => (dir > 0 ? p >= v0 : p <= v0)).sort((a, b) => dir * (a - b));
for (const p of points) {
const dist = Math.abs(p - v) * ppu;
if (rest <= dist) break;
rest -= dist; v = p;
if (rest <= o.snapPixels) { held = p; rest = 0; break; }
rest -= o.snapPixels;
if (rest <= o.detentPixels) { held = p; rest = 0; break; }
rest -= o.detentPixels;
}
const target = held ?? v + dir * rest / ppu;
const nv = clamp(held ?? round(target));
@@ -169,7 +169,7 @@
try { stockHaptic.call(ov, ov.ThisOverlayHandle(), effect); hits.haptics++; } catch (e) { log('haptic failed', String(e)); }
};
const hapticFor = (v) => haptic(v <= 0 || v >= o.max ? HAPTIC.SlidingEdge
: o.snapPoints.includes(v) ? HAPTIC.Snap : HAPTIC.Sliding);
: o.detentPoints.includes(v) ? HAPTIC.Snap : HAPTIC.Sliding);
// ---- scene graph ---------------------------------------------------------------------
// Origin node ids may carry a "<prefix>::" (sub-scene); match the suffix.
@@ -338,7 +338,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
// Presses/clicks are stopped at the element so React (root listener) never
// runs the stock onClick; a press that never became a drag is a click on
// mouseup. Drag is vertical and relative (the cylinder bends horizontally,
// so y barely moves as curvature changes); after dragThreshold px it
// so y barely moves as curvature changes); after dragThresholdPixels px it
// re-bases (no jump). One press at a time, window capture listeners.
const stop = (e) => e.stopPropagation();
const STOPPED = ['click', 'dblclick', 'mouseup', 'pointerdown', 'pointerup', 'contextmenu'];
@@ -359,7 +359,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
const y = e.clientY;
p.last = y;
if (!p.moved) {
if (Math.abs(y - p.y0) < o.dragThreshold) return;
if (Math.abs(y - p.y0) < o.dragThresholdPixels) return;
p.moved = true;
muteStock = true;
p.c.el?.classList.add('sfui-dragging');
@@ -543,6 +543,6 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
log('patched', { version: VERSION, opts: o });
resend();
queueSync();
return `patched (default ${o.default}, max ${o.max}, step ${o.step}, snap ${o.snapPixels} px at ${o.snapPoints.join('/')}, ` +
return `patched (initial ${o.initial}, max ${o.max}, step ${o.step}, detent ${o.detentPixels} px at ${o.detentPoints.join('/')}, ` +
`haptics ${o.haptics ? 'on' : 'off'})`;
})