ananyajoshi Claude Opus 5 (1M context) commited on
Commit
932e21f
Β·
1 Parent(s): a722c74

Fix buttons needing several clicks: stop control-bearing panels drifting

Browse files

Save Configuration and Run API Check often ignored a click. The Python
handler was never at fault β€” it saves correctly on one click. The cause
was in the browser: the ambient float3d loop runs on every surface in
SURFACES, which includes [class*="st-key-ups_panel"], so the panel
holding the buttons carried a perpetual inline transform
(translateZ + rotateX, repeat: Infinity).

Measured on the running app: the Save button drifted 7.4px horizontally
and 4.3px vertically, continuously. A click only fires when mousedown
and mouseup land on the same element, so the moving button silently
swallowed clicks β€” Playwright could not click it at all, reporting
"element is not stable" across 56 retries in 30s.

motion_runtime already stated this rule for buttons ("a button that
moves under the cursor is a button that gets mis-clicked"); it just
never applied it to the surface carrying the button. Adds canDrift() β€”
a stricter sibling of canMove() β€” and gates float3d, tilt3d and the
.ups-ev depth wave on it. Entrance animations are untouched: they are
finite and settle. Presentation-only surfaces still float, tilt and
catch the sheen, so the design is unchanged.

After: all four buttons measure 0.0px drift and save on one click.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. app/ui/motion_runtime.py +28 -2
app/ui/motion_runtime.py CHANGED
@@ -101,6 +101,27 @@ _DRIVER = r"""
101
  return true;
102
  }
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  // ── Depth ────────────────────────────────────────────────────────────
105
  // A translateZ only reads as depth when an ancestor carries a
106
  // perspective, and a perspective β€” like a transform β€” makes that
@@ -132,7 +153,7 @@ _DRIVER = r"""
132
  // long as the node lives, so nothing else may clear it. Anything that wants
133
  // the transform (hover, press, tilt) pauses this first and resumes after.
134
  function float3d(el, i) {
135
- if (!canMove(el)) return;
136
  depthParent(el);
137
  el.style.transformStyle = 'preserve-3d';
138
  var ph = (i % 5) / 5;
@@ -158,6 +179,9 @@ _DRIVER = r"""
158
  // Pointer tilt. Hands over from the float and hands back on leave, because
159
  // two animations writing the same transform component fight each other.
160
  function tilt3d(el, max) {
 
 
 
161
  var frame = 0;
162
  el.addEventListener('pointerenter', function () { holdFloat(el); });
163
  el.addEventListener('pointermove', function (e) {
@@ -342,7 +366,9 @@ _DRIVER = r"""
342
  root.querySelectorAll('.ups-ev').forEach(function (el, i) {
343
  if (el.dataset.upsD) return;
344
  el.dataset.upsD = '1';
345
- if (!canMove(el)) return;
 
 
346
  depthParent(el);
347
  el.style.transformStyle = 'preserve-3d';
348
  el.__upsFloat = M.animate(el, {
 
101
  return true;
102
  }
103
 
104
+ // Perpetual motion is a stricter test than canMove.
105
+ //
106
+ // A `click` fires only when mousedown and mouseup land on the SAME element.
107
+ // A surface that drifts for as long as it lives β€” or turns under the
108
+ // pointer β€” moves every control it contains out from under the cursor
109
+ // between those two events, so clicks are silently swallowed and the user
110
+ // has to stab at the button several times before one lands. That is the
111
+ // same rule the button handler further down already states ("a button that
112
+ // moves under the cursor is a button that gets mis-clicked"); it just has
113
+ // to apply to the surface CARRYING the button, not only to the button.
114
+ //
115
+ // Entrance animations are deliberately exempt: they are finite and settle,
116
+ // so the arrival choreography is unchanged. Only never-ending motion is
117
+ // withheld, and only from surfaces that actually hold controls β€” a KPI
118
+ // tile or a verdict card still floats and tilts exactly as before.
119
+ var CONTROLS = 'button, input, select, textarea, a[href], [role="button"], ' +
120
+ '[role="tab"], [role="option"], [contenteditable="true"]';
121
+ function canDrift(el) {
122
+ return canMove(el) && !el.querySelector(CONTROLS) && !el.matches(CONTROLS);
123
+ }
124
+
125
  // ── Depth ────────────────────────────────────────────────────────────
126
  // A translateZ only reads as depth when an ancestor carries a
127
  // perspective, and a perspective β€” like a transform β€” makes that
 
153
  // long as the node lives, so nothing else may clear it. Anything that wants
154
  // the transform (hover, press, tilt) pauses this first and resumes after.
155
  function float3d(el, i) {
156
+ if (!canDrift(el)) return;
157
  depthParent(el);
158
  el.style.transformStyle = 'preserve-3d';
159
  var ph = (i % 5) / 5;
 
179
  // Pointer tilt. Hands over from the float and hands back on leave, because
180
  // two animations writing the same transform component fight each other.
181
  function tilt3d(el, max) {
182
+ // Guarded here rather than at each call site so no future caller can
183
+ // reintroduce a control-bearing surface that moves under the pointer.
184
+ if (!canDrift(el)) return;
185
  var frame = 0;
186
  el.addEventListener('pointerenter', function () { holdFloat(el); });
187
  el.addEventListener('pointermove', function (e) {
 
366
  root.querySelectorAll('.ups-ev').forEach(function (el, i) {
367
  if (el.dataset.upsD) return;
368
  el.dataset.upsD = '1';
369
+ // repeat: Infinity below β€” same rule as float3d: an entry that never
370
+ // settles must not be carrying anything clickable.
371
+ if (!canDrift(el)) return;
372
  depthParent(el);
373
  el.style.transformStyle = 'preserve-3d';
374
  el.__upsFloat = M.animate(el, {