Spaces:
Sleeping
Sleeping
Download app/ui/motion_runtime.py from SkillArbitrage/UpworkAutomation: direct link, hf CLI and curl.
- Browser
- Download file 27.6 kB
-
https://huggingface.co/spaces/SkillArbitrage/UpworkAutomation/resolve/main/app/ui/motion_runtime.py
- Command line
-
hf download hf://spaces/SkillArbitrage/UpworkAutomation/app/ui/motion_runtime.py
-
curl -L -o motion_runtime.py https://huggingface.co/spaces/SkillArbitrage/UpworkAutomation/resolve/main/app/ui/motion_runtime.py
27.6 kB
| """Motion β real animation on the live Streamlit DOM. | |
| Why a runtime at all, when there is already a CSS motion layer: CSS can only | |
| animate what it can select, from states it can express. It cannot start an | |
| animation when a panel scrolls into view, it cannot run a spring, and it cannot | |
| count a number up to the value the app just computed. Motion does all three. | |
| How it reaches the page. ``st.markdown`` strips <script>, so nothing can be | |
| executed from the markup the screens emit. ``components.html`` runs scripts, | |
| but only inside its own sandboxed iframe. That iframe is same-origin with the | |
| app (Streamlit sets it via srcdoc), so it can reach ``window.parent`` β the | |
| same bridge :func:`app.ui.theme.apply_step_accent` already uses for the theme. | |
| So the iframe is only a bootstrapper: it injects two <script> elements into the | |
| PARENT document β the library, then the driver β and both then run in the | |
| parent's own realm. That matters because Streamlit may discard the component | |
| iframe on a rerun; anything still living in the iframe's realm would die with | |
| it. Both injections are idempotent, and the driver installs a MutationObserver, | |
| so it keeps animating the nodes Streamlit swaps in on every rerun without the | |
| iframe having to run again. | |
| Two rules the animations here must never break: | |
| * **Nothing that is, or contains, a file uploader may be transformed.** A | |
| transform makes an element the containing block for its ``position: fixed`` | |
| descendants, which is what broke dossier drag-and-drop once already. Those | |
| surfaces are animated by opacity alone (see ``canMove``). | |
| * **No element is ever left hidden.** Every "from" value lives in a keyframe | |
| array, never in CSS and never written to the element up front, so a failure | |
| to animate leaves content visible and correct rather than stranded at | |
| ``opacity: 0``. | |
| * **Perspective and ``transform-style: preserve-3d`` follow the same rule as a | |
| transform.** Both make an element the containing block for its | |
| ``position: fixed`` descendants. So the depth layer never declares them in | |
| CSS on a shared ancestor β the driver sets them at runtime on an individual | |
| surface's parent, and only when that parent passes ``canMove``. Putting a | |
| perspective on ``.block-container`` would break the uploader app-wide. | |
| The library is vendored (``app/assets/vendor/motion.umd.js``, MIT) rather than | |
| loaded from a CDN, because this app also ships as an offline desktop launcher. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from functools import lru_cache | |
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| from app.paths import resource_path | |
| def _library() -> str: | |
| """The vendored Motion UMD bundle. Empty string if it is missing.""" | |
| try: | |
| return resource_path("app", "assets", "vendor", "motion.umd.js").read_text( | |
| encoding="utf-8" | |
| ) | |
| except Exception: | |
| return "" | |
| # The driver. Runs in the parent realm; `Motion` is the UMD global. | |
| _DRIVER = r""" | |
| (function () { | |
| if (window.__upsMotion) { window.__upsMotion.rescan(); return; } | |
| var M = window.Motion; | |
| if (!M || !M.animate) return; | |
| // The ambient panel float β the perpetual translateZ/rotate loop added in | |
| // "UI: 3D depth layer" β is OFF unless UPS_PANEL_FLOAT is set. | |
| // | |
| // It is the one effect that never settles, and a surface that never settles | |
| // drags its buttons out from under the pointer: a click fires only when | |
| // mousedown and mouseup land on the same element. Measured against that | |
| // commit, Save Configuration failed to save on 4 of 4 attempts; on the | |
| // commit before it, and with the float off, it saves every time. | |
| // | |
| // Freezing it near the pointer (below) fixes this in principle and in every | |
| // automated test, but not reliably in the hand, so the working button wins | |
| // over the effect. Set UPS_PANEL_FLOAT=1 to bring the drift back β the | |
| // proximity-freeze machinery below stays in place for that case. | |
| var FLOAT_ON = false; | |
| try { FLOAT_ON = !!window.__upsPanelFloat; } catch (e) {} | |
| var EASE = [0.22, 0.61, 0.36, 1]; | |
| var SPRING = { type: 'spring', stiffness: 320, damping: 30, mass: 0.9 }; | |
| var reduce = false; | |
| try { | |
| // UPS_REDUCE_MOTION=1 forces this on from the server side β an escape | |
| // hatch that needs no OS setting and no code edit, for when the motion | |
| // layer has to be taken out of the picture (e.g. to determine whether it | |
| // is responsible for an interaction problem). It routes through the same | |
| // already-proven bail-out as the accessibility preference below rather | |
| // than adding a second, less-tested way to switch motion off. | |
| reduce = !!window.__upsReduceMotion || | |
| window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| } catch (e) {} | |
| // Transforms are always written as Motion's discrete properties (y, scale, | |
| // scaleX) and never as a transform STRING. Motion decomposes a string into | |
| // its components, and a "none" end value resolves the scale component to 0, | |
| // not 1 β which leaves the element occupying its space and painting nothing. | |
| // | |
| // And every entrance clears the inline transform once it has finished, so no | |
| // surface is left permanently transformed: a transform makes an element the | |
| // containing block for its fixed-position descendants. | |
| function settle(anim, el) { | |
| if (!anim || !anim.then) return; | |
| anim.then(function () { el.style.transform = ''; }, function () {}); | |
| } | |
| // A transform on one of these would become the containing block for the | |
| // uploader's fixed-position drop overlay. Opacity only, always. | |
| function canMove(el) { | |
| return !el.querySelector('[data-testid="stFileUploader"]') && | |
| !el.closest('[data-testid="stFileUploader"]'); | |
| } | |
| function once(el) { | |
| if (el.dataset.upsM) return false; | |
| el.dataset.upsM = '1'; | |
| return true; | |
| } | |
| // ββ Depth ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // A translateZ only reads as depth when an ancestor carries a | |
| // perspective, and a perspective β like a transform β makes that | |
| // ancestor the containing block for fixed-position descendants. So it | |
| // goes on the surface's own parent, one at a time, and only through | |
| // canMove(). A surface whose parent is disqualified still animates; it | |
| // simply stays flat, which is the correct fallback. | |
| function depthParent(el) { | |
| var p = el.parentElement; | |
| if (!p || p.dataset.upsP || !canMove(p)) return false; | |
| p.dataset.upsP = '1'; | |
| p.style.perspective = '1200px'; | |
| p.style.perspectiveOrigin = '50% 42%'; | |
| return true; | |
| } | |
| // Five samples of a sine, phase-shifted. First and last are equal, so the | |
| // loop closes seamlessly; the phase is what stops a grid of cards from | |
| // breathing in unison, which is what makes ambient motion look mechanical. | |
| function wave(amp, phase, offset) { | |
| var k = [], t; | |
| for (t = 0; t <= 4; t++) { | |
| k.push(+((offset || 0) + amp * Math.sin(2 * Math.PI * (t / 4 + phase))).toFixed(3)); | |
| } | |
| return k; | |
| } | |
| // The ambient loop. Never settles β it owns the element's transform for as | |
| // long as the node lives, so nothing else may clear it. Anything that wants | |
| // the transform (hover, press, tilt) pauses this first and resumes after. | |
| function float3d(el, i) { | |
| // OFF by default β see FLOAT_ON. This is the single choke point for the | |
| // perpetual drift, so gating here leaves every other piece of the motion | |
| // layer (entrances, press, hover lift, tilt on presentation cards, sheen, | |
| // ring draw, counters) running exactly as designed. | |
| if (!FLOAT_ON) return; | |
| if (!canMove(el)) return; | |
| depthParent(el); | |
| el.style.transformStyle = 'preserve-3d'; | |
| var ph = (i % 5) / 5; | |
| el.__upsFloat = M.animate(el, { | |
| rotateY: wave(5.5, ph), | |
| rotateX: wave(3.2, ph + 0.25), | |
| z: wave(11, ph + 0.5, 11) | |
| }, { | |
| duration: 9 + (i % 4) * 1.5, | |
| repeat: Infinity, | |
| ease: 'easeInOut' | |
| }); | |
| // Keeps the drift, but holds it still while the pointer is inside a | |
| // surface the user can click into. See steadyForPointer below. | |
| steadyForPointer(el); | |
| // The float is created AFTER the entrance, which routinely finishes under | |
| // a cursor that is already resting on the panel. Starting it running would | |
| // drift the button out from under a press that is about to happen, so ask | |
| // :hover directly rather than waiting for an event that will never come. | |
| // It begins held; pointerleave releases it. | |
| if (el.__upsInside || pointerIsOver(el)) { | |
| el.__upsInside = 1; | |
| holdFloat(el); | |
| } | |
| } | |
| function holdFloat(el) { | |
| if (el.__upsFloat) { try { el.__upsFloat.pause(); } catch (e) {} } | |
| } | |
| function resumeFloat(el) { | |
| if (el.__upsFloat) { try { el.__upsFloat.play(); } catch (e) {} return true; } | |
| return false; | |
| } | |
| // A surface that carries controls keeps its ambient float β it just holds | |
| // still while you are aiming at something inside it. | |
| // | |
| // A `click` fires only when mousedown and mouseup land on the SAME element. | |
| // A panel drifting a few pixels moves its buttons out from under the cursor | |
| // between those two events, so clicks are silently swallowed and the button | |
| // appears to need several stabs before one registers. Freezing on | |
| // pointerenter makes the surface stationary for exactly as long as the | |
| // pointer is inside it, and the drift resumes on the way out. | |
| // | |
| // This is the same handoff tilt3d performs, through the same pause/play | |
| // pair, so nothing new ever owns the transform: the float stays its sole | |
| // author and simply stops advancing. Pausing (rather than clearing) holds | |
| // the current transform, so the surface never snaps. | |
| var CONTROLS = 'button, input, select, textarea, a[href], [role="button"], ' + | |
| '[role="tab"], [role="option"], [contenteditable="true"]'; | |
| // Armed the moment a surface is registered β BEFORE its entrance runs and | |
| // long before the float starts. Attaching it later (say, when the float is | |
| // created) leaves a hole: pointerenter does not re-fire for a pointer that | |
| // is already inside, so a user whose cursor was over the panel while it | |
| // arrived would never freeze it, and the button would drift out from under | |
| // the press. That hole is what made the first attempt at this fix look like | |
| // it worked under test and still fail in the hand. | |
| // | |
| // `__upsInside` carries the state across that ordering: it is the flag | |
| // float3d consults so a float created while the pointer is already inside | |
| // starts paused rather than immediately drifting. | |
| // | |
| // pointermove is a deliberate second trigger, not redundancy: it fires for | |
| // a pointer that was already inside when the listeners went on, which is | |
| // exactly the case pointerenter cannot see. | |
| // Is the pointer over this surface RIGHT NOW? :hover is the only honest | |
| // answer available without an event, and an event is exactly what we cannot | |
| // rely on here: a cursor that came to rest on the panel before the float | |
| // existed fires no pointerenter (it never re-enters) and no pointermove (it | |
| // never moves). That resting cursor is the common case β the user is | |
| // already looking at the form, reaching for the button β and it is why the | |
| // earlier event-only attempts still failed in the hand while passing tests | |
| // that moved the mouse. | |
| function pointerIsOver(el) { | |
| try { return el.matches(':hover'); } catch (e) { return false; } | |
| } | |
| // Freezing when the pointer ARRIVES is too late. The surface is still | |
| // drifting throughout the approach, so the button moves while the hand is | |
| // travelling to it and the press lands where the button used to be β which | |
| // reads, correctly, as "the button did nothing". The surface therefore has | |
| // to be still BEFORE the pointer gets there. | |
| // | |
| // So proximity, not containment: a control-bearing surface holds as soon as | |
| // the pointer is within MARGIN of its box, and drifts again once the pointer | |
| // moves away. One throttled document-level listener drives all of them, | |
| // which also sidesteps every attach-ordering trap that sank the earlier | |
| // attempts β it does not matter when a surface was registered, only where | |
| // the pointer is on the next move. | |
| // NOTE ON LIFETIME: inject() appends this whole driver to the parent | |
| // document on EVERY Streamlit rerun, so this file's top level runs many | |
| // times per page. Anything registered here must therefore be idempotent β | |
| // both the surface list and the listener hang off `window`, guarded, or a | |
| // busy session would accumulate one listener and one orphaned list per | |
| // rerun, each holding nodes the others cannot see. | |
| var STEADY = (window.__upsSteady = window.__upsSteady || []); | |
| var STEADY_MARGIN = 140; // px of clearance β stops before you arrive | |
| function steadyForPointer(el) { | |
| if (el.dataset.upsSteady) return; | |
| if (!el.querySelector(CONTROLS)) return; // presentation surfaces drift freely | |
| el.dataset.upsSteady = '1'; | |
| STEADY.push(el); | |
| } | |
| if (!window.__upsSteadyBound) { | |
| window.__upsSteadyBound = 1; | |
| var steadyFrame = 0; | |
| document.addEventListener('pointermove', function (e) { | |
| if (steadyFrame) return; | |
| var px = e.clientX, py = e.clientY; | |
| steadyFrame = requestAnimationFrame(function () { | |
| steadyFrame = 0; | |
| var list = window.__upsSteady || []; | |
| for (var i = list.length - 1; i >= 0; i--) { | |
| var el = list[i]; | |
| // Streamlit replaces nodes on every rerun; drop the stale ones. | |
| if (!el.isConnected) { list.splice(i, 1); continue; } | |
| var r = el.getBoundingClientRect(); | |
| var near = px >= r.left - STEADY_MARGIN && px <= r.right + STEADY_MARGIN && | |
| py >= r.top - STEADY_MARGIN && py <= r.bottom + STEADY_MARGIN; | |
| if (near && !el.__upsInside) { el.__upsInside = 1; holdFloat(el); } | |
| else if (!near && el.__upsInside) { el.__upsInside = 0; resumeFloat(el); } | |
| } | |
| }); | |
| }, true); | |
| } | |
| // Pointer tilt. Hands over from the float and hands back on leave, because | |
| // two animations writing the same transform component fight each other. | |
| function tilt3d(el, max) { | |
| var frame = 0; | |
| el.addEventListener('pointerenter', function () { holdFloat(el); }); | |
| el.addEventListener('pointermove', function (e) { | |
| if (frame) return; | |
| frame = requestAnimationFrame(function () { | |
| frame = 0; | |
| var r = el.getBoundingClientRect(); | |
| if (!r.width || !r.height) return; | |
| var px = (e.clientX - r.left) / r.width; | |
| var py = (e.clientY - r.top) / r.height; | |
| M.animate(el, { | |
| rotateY: (px - 0.5) * 2 * max, | |
| rotateX: (0.5 - py) * 2 * max, | |
| z: 30 | |
| }, { type: 'spring', stiffness: 260, damping: 26 }); | |
| }); | |
| }); | |
| el.addEventListener('pointerleave', function () { | |
| var back = M.animate(el, { rotateX: 0, rotateY: 0, z: 0 }, | |
| { type: 'spring', stiffness: 180, damping: 22 }); | |
| if (back && back.then) { | |
| back.then(function () { | |
| if (!resumeFloat(el)) el.style.transform = ''; | |
| }, function () {}); | |
| } | |
| }); | |
| } | |
| // Panels, cards and tiles arrive as they scroll into view β not all at once | |
| // on load, which is what made the page read as a still photograph. | |
| var SURFACES = [ | |
| '[class*="st-key-ups_panel"]', | |
| '.card', '.verdict', '.proposal-doc', | |
| '.ups-panel', '.ups-kpi', '.ups-kv', '.ups-log', | |
| '[data-testid="stAlert"]' | |
| ].join(','); | |
| // Their contents cascade behind them. | |
| var ROWS = '.ups-row, .ups-ev, .checkitem, .ups-kvc, .ups-thead'; | |
| function enter(el, i) { | |
| var move = canMove(el); | |
| M.inView(el, function () { | |
| var anim = M.animate( | |
| el, | |
| move ? { opacity: [0, 1], y: [16, 0], z: [-140, 0], rotateX: [7, 0] } | |
| : { opacity: [0, 1] }, | |
| { duration: 0.56, ease: EASE } | |
| ); | |
| // The ambient float takes over the transform the moment the entrance | |
| // lets go of it, so the surface never sits perfectly still. | |
| if (move && anim && anim.then) { | |
| anim.then(function () { | |
| el.style.transform = ''; | |
| float3d(el, i || 0); | |
| }, function () {}); | |
| } | |
| }, { amount: 0.12 }); | |
| } | |
| function cascade(container) { | |
| var items = container.querySelectorAll(ROWS); | |
| if (!items.length) return; | |
| M.inView(container, function () { | |
| var anim = M.animate( | |
| items, | |
| { opacity: [0, 1], y: [10, 0] }, | |
| { duration: 0.42, ease: EASE, delay: M.stagger(0.045) } | |
| ); | |
| if (anim && anim.then) { | |
| anim.then(function () { | |
| items.forEach(function (it) { it.style.transform = ''; }); | |
| }, function () {}); | |
| } | |
| }, { amount: 0.1 }); | |
| } | |
| // Numbers arrive at their value rather than appearing at it. Only the | |
| // leading number is animated, so "78/100" and "$2,000" keep their shape. | |
| function countUp(el) { | |
| var raw = (el.textContent || '').trim(); | |
| var m = raw.match(/^(-?\d[\d,]*)(.*)$/s); | |
| if (!m) return; | |
| var target = parseFloat(m[1].replace(/,/g, '')); | |
| if (!isFinite(target) || target === 0) return; | |
| var tail = m[2], grouped = m[1].indexOf(',') !== -1; | |
| M.inView(el, function () { | |
| M.animate(0, target, { | |
| duration: 0.9, | |
| ease: EASE, | |
| onUpdate: function (v) { | |
| var n = Math.round(v); | |
| el.textContent = (grouped ? n.toLocaleString() : String(n)) + tail; | |
| } | |
| }); | |
| }, { amount: 0.5 }); | |
| } | |
| // The fit ring draws itself to the score, comet head and all. | |
| function drawRing(svgRoot) { | |
| var bar = svgRoot.querySelector('.bar'); | |
| var comet = svgRoot.querySelector('.comet'); | |
| if (!bar) return; | |
| var total = parseFloat(bar.getAttribute('stroke-dasharray')) || 433.5; | |
| var target = parseFloat(bar.getAttribute('stroke-dashoffset')); | |
| if (!isFinite(target)) return; | |
| var deg = 360 * (1 - target / total); | |
| var host = svgRoot.closest('.ups-ring') || svgRoot; | |
| M.inView(svgRoot, function () { | |
| M.animate(bar, { strokeDashoffset: [total, target] }, | |
| { duration: 1.1, ease: EASE }); | |
| if (comet) M.animate(comet, { rotate: [0, deg] }, { duration: 1.1, ease: EASE }); | |
| // The disc lies back as it draws, then rocks like a gauge that has not | |
| // quite settled. Kept shallow β the score has to stay readable. | |
| if (!canMove(host)) return; | |
| depthParent(host); | |
| host.style.transformStyle = 'preserve-3d'; | |
| var lie = M.animate(host, { rotateX: [34, 12] }, { duration: 1.1, ease: EASE }); | |
| if (lie && lie.then) { | |
| lie.then(function () { | |
| host.__upsFloat = M.animate(host, { | |
| rotateX: wave(3.5, 0, 12), | |
| rotateY: wave(5, 0.25) | |
| }, { duration: 8, repeat: Infinity, ease: 'easeInOut' }); | |
| }, function () {}); | |
| } | |
| }, { amount: 0.4 }); | |
| } | |
| // Meters fill from empty to the width the app computed. | |
| function fillMeter(fill) { | |
| var target = fill.style.width; | |
| if (!target) return; | |
| M.inView(fill, function () { | |
| M.animate(fill, { width: ['0%', target] }, { duration: 0.8, ease: EASE }); | |
| }, { amount: 0.6 }); | |
| } | |
| // Hover and press are springs, so the whole surface feels physical. The | |
| // inline transform is cleared on the way out, never left behind. | |
| function springy(el, lift) { | |
| M.hover(el, function () { | |
| holdFloat(el); | |
| M.animate(el, { y: lift }, SPRING); | |
| return function () { | |
| var back = M.animate(el, { y: 0 }, SPRING); | |
| if (back && back.then) { | |
| back.then(function () { | |
| // Only clear the transform if nothing owns it. Clearing it out | |
| // from under a running float leaves the surface snapping. | |
| if (!resumeFloat(el)) el.style.transform = ''; | |
| }, function () {}); | |
| } | |
| }; | |
| }); | |
| M.press(el, function () { | |
| M.animate(el, { scale: 0.985, z: -18 }, { duration: 0.08, ease: 'easeOut' }); | |
| return function () { | |
| var back = M.animate(el, { scale: 1, z: 0 }, SPRING); | |
| if (back && back.then) { | |
| back.then(function () { | |
| if (!resumeFloat(el)) el.style.transform = ''; | |
| }, function () {}); | |
| } | |
| }; | |
| }); | |
| } | |
| function rescan() { | |
| var root = document.querySelector('[data-testid="stAppViewContainer"]') || document.body; | |
| if (!root) return; | |
| root.querySelectorAll(SURFACES).forEach(function (el, i) { | |
| if (!once(el)) return; | |
| // Armed first, before the entrance and long before the float exists, so | |
| // a pointer resting on the panel as it arrives is already accounted for. | |
| steadyForPointer(el); | |
| enter(el, i); | |
| cascade(el); | |
| }); | |
| // Evidence entries ride a travelling depth wave, so a findings list reads | |
| // as a stack of physical cards rather than a table. | |
| root.querySelectorAll('.ups-ev').forEach(function (el, i) { | |
| if (el.dataset.upsD) return; | |
| el.dataset.upsD = '1'; | |
| if (!canMove(el)) return; | |
| depthParent(el); | |
| el.style.transformStyle = 'preserve-3d'; | |
| el.__upsFloat = M.animate(el, { | |
| z: wave(17, (i % 4) / 4, 17), | |
| rotateX: wave(2.4, (i % 4) / 4 + 0.25) | |
| }, { duration: 6.5, repeat: Infinity, ease: 'easeInOut' }); | |
| // Same rule as float3d: an entry that never settles holds still while | |
| // the pointer is inside it, in case it ever carries a link or control. | |
| steadyForPointer(el); | |
| }); | |
| // The mark is a compass. Turning is the one thing it should do. | |
| root.querySelectorAll('.ups-brand-mark').forEach(function (el) { | |
| if (el.dataset.upsD) return; | |
| el.dataset.upsD = '1'; | |
| M.animate(el, { rotateY: [0, 360] }, | |
| { duration: 14, repeat: Infinity, ease: 'linear' }); | |
| }); | |
| root.querySelectorAll('.ups-kpi .val, .ups-ring .val b').forEach(function (el) { | |
| if (once(el)) countUp(el); | |
| }); | |
| root.querySelectorAll('.ups-ring svg').forEach(function (el) { | |
| if (once(el)) drawRing(el); | |
| }); | |
| root.querySelectorAll('.ups-meter i').forEach(function (el) { | |
| if (once(el)) fillMeter(el); | |
| }); | |
| // The tiles lift; buttons only press, because a button that moves under | |
| // the cursor is a button that gets mis-clicked. | |
| root.querySelectorAll('.ups-kpi').forEach(function (el) { | |
| if (el.dataset.upsH) return; | |
| el.dataset.upsH = '1'; | |
| if (canMove(el)) { springy(el, -4); tilt3d(el, 9); } | |
| }); | |
| // The verdict and the analysis cards turn under the pointer. Panels are | |
| // left alone: a whole panel tilting is seasick, not premium. | |
| root.querySelectorAll('.verdict, .card').forEach(function (el) { | |
| if (el.dataset.upsH) return; | |
| el.dataset.upsH = '1'; | |
| if (canMove(el)) tilt3d(el, 7); | |
| }); | |
| root.querySelectorAll('.stButton button, .stDownloadButton button').forEach(function (el) { | |
| if (el.dataset.upsH) return; | |
| el.dataset.upsH = '1'; | |
| M.press(el, function () { | |
| M.animate(el, { scale: 0.97 }, { duration: 0.08, ease: 'easeOut' }); | |
| return function () { settle(M.animate(el, { scale: 1 }, SPRING), el); }; | |
| }); | |
| }); | |
| // The step you are on pulses into place when it changes. | |
| root.querySelectorAll('.ups-seg-i.is-now').forEach(function (el) { | |
| if (!once(el)) return; | |
| settle(M.animate(el, { scaleX: [0.2, 1] }, | |
| { type: 'spring', stiffness: 400, damping: 22 }), el); | |
| }); | |
| } | |
| if (reduce) { | |
| // Nothing is hidden up front, so honouring the setting means simply not | |
| // starting anything. The page is already in its final state. | |
| window.__upsMotion = { rescan: function () {}, reduced: true }; | |
| return; | |
| } | |
| var pending = 0; | |
| function schedule() { | |
| if (pending) return; | |
| pending = requestAnimationFrame(function () { pending = 0; rescan(); }); | |
| } | |
| // Streamlit replaces DOM nodes on every rerun, so the driver watches for | |
| // them rather than running once and assuming the page stays put. | |
| var target = document.querySelector('[data-testid="stAppViewContainer"]') || document.body; | |
| var observer = new MutationObserver(schedule); | |
| observer.observe(target, { childList: true, subtree: true }); | |
| window.__upsMotion = { rescan: rescan, observer: observer, reduced: false }; | |
| rescan(); | |
| })(); | |
| """ | |
| def inject() -> None: | |
| """Load Motion into the page and start the driver. Safe to call every run. | |
| Fails silently and completely: if the bundle is missing or the parent | |
| document is unreachable, the app simply keeps the CSS motion layer. | |
| """ | |
| lib = _library() | |
| if not lib: | |
| return | |
| # Both payloads ride inside <script> elements, so nothing in them may | |
| # contain a closing tag. Neither does today; this keeps that true after an | |
| # upgrade. In JavaScript "<\\/script" and "</script" are the same string. | |
| lib = lib.replace("</script", "<\\/script") | |
| driver = _DRIVER.replace("</script", "<\\/script") | |
| # Server-side motion kill switch. Set UPS_REDUCE_MOTION=1 to load the | |
| # driver but start nothing, exactly as the OS accessibility preference | |
| # does. Useful for isolating whether the motion layer is behind an | |
| # interaction problem without touching code or system settings. | |
| reduce_motion = (os.getenv("UPS_REDUCE_MOTION", "") or "").strip().lower() in ( | |
| "1", "true", "yes", "on" | |
| ) | |
| # Opt-in for the perpetual panel drift. Off by default because it moves | |
| # buttons out from under a click; see FLOAT_ON in the driver. | |
| panel_float = (os.getenv("UPS_PANEL_FLOAT", "") or "").strip().lower() in ( | |
| "1", "true", "yes", "on" | |
| ) | |
| components.html( | |
| # The payloads come first: the bootstrap below reads them by id, and a | |
| # <script> only sees the document that exists above it. | |
| '<script type="text/plain" id="ups-motion-lib">' + lib + "</script>\n" | |
| '<script type="text/plain" id="ups-motion-driver">' + driver + "</script>\n" | |
| "<script>\n" | |
| "(function () {\n" | |
| " var libEl = document.getElementById('ups-motion-lib');\n" | |
| " var drvEl = document.getElementById('ups-motion-driver');\n" | |
| " if (!libEl || !drvEl) return;\n" | |
| " var LIB = libEl.textContent, DRIVER = drvEl.textContent;\n" | |
| " function boot() {\n" | |
| " try {\n" | |
| " var w = window.parent, d = w.document;\n" | |
| " if (!d || !d.head) return false;\n" | |
| " w.__upsReduceMotion = " + ("true" if reduce_motion else "false") + ";\n" | |
| " w.__upsPanelFloat = " + ("true" if panel_float else "false") + ";\n" | |
| " if (!w.Motion) {\n" | |
| " var s = d.createElement('script');\n" | |
| " s.id = 'ups-motion-lib';\n" | |
| " s.textContent = LIB;\n" | |
| " d.head.appendChild(s);\n" | |
| " }\n" | |
| " if (!w.Motion) return false;\n" | |
| " var t = d.createElement('script');\n" | |
| " t.textContent = DRIVER;\n" | |
| " d.head.appendChild(t);\n" | |
| " t.remove();\n" | |
| " return true;\n" | |
| " } catch (e) { return false; }\n" | |
| " }\n" | |
| " if (!boot()) {\n" | |
| " var n = 0;\n" | |
| " var iv = setInterval(function () {\n" | |
| " if (boot() || ++n > 20) clearInterval(iv);\n" | |
| " }, 50);\n" | |
| " }\n" | |
| "})();\n" | |
| "</script>", | |
| height=0, | |
| ) | |