gui: spinners turn round their own centre: the drawing turns inside the svg, round the viewBox's centre, since WebKit turned a whole svg on a pixel-snapped layer and its ring circled a point ~1.3px off (Agents' "Updating…", Sync, Save, the quota and usage refreshes, the agent fix) (yetone)
Measured on the real screen (headed WebKit + screencapture, 30 frames, circle fit): the ring's centre wandered 2.7 device px before, 0.2 after. Playwright screenshots repaint and never showed it. The CSS ring spinners (sign-in, plugins) measured 0.2 already and are unchanged. #sync's stop-on-a-whole-turn reads the svg's subtree animations. Run: cli-update (fails on the old CSS), agent-update-all, sync-spin, provider-save-busy, agent-app-version, agent-unreachable, aside-native, usage-card-refresh, usage-remote-cards, usage-stale-answer: all pass, Chromium + WebKit.
This commit is contained in:
1 parent
05b5965872
commit
97f825d560
5 files changed
+22
-14
No files matched your search
@@ -227,7 +227,10 @@ body.panel .seg { display: none; }
|
||||
/* the icon turns on a layer of its own — painted in place, WebKit leaves
|
||||
ghosts of each frame behind (#16) — and always finishes its turn */
|
||||
.icon svg { transform-origin: 50% 50%; backface-visibility: hidden; }
|
||||
.icon.spin svg { animation: spin .9s linear infinite; will-change: transform; }
|
||||
/* a spinner turns its drawing inside the svg, round the viewBox's centre:
|
||||
WebKit turns a whole svg on a layer snapped to the pixel grid, so off a
|
||||
whole pixel its ring circles about a point a pixel or so off its centre */
|
||||
.icon.spin svg > * { transform-box: view-box; transform-origin: 50% 50%; animation: spin .9s linear infinite; }
|
||||
/* a newer magpie is ready: a quiet pill before the header's icons */
|
||||
.update {
|
||||
align-self: center; display: inline-flex; align-items: center; gap: 4px;
|
||||
@@ -395,7 +398,7 @@ body.panel #agents > .row .ag-handle::before { left: -10px; }
|
||||
.ag-fix:active { transform: scale(.96); }
|
||||
.ag-fix:focus-visible { outline: 1.5px solid var(--drift); outline-offset: 1px; }
|
||||
.ag-fix.busy { pointer-events: none; opacity: .7; }
|
||||
.ag-fix.busy svg { animation: spin .8s linear infinite; }
|
||||
.ag-fix.busy svg > * { transform-box: view-box; transform-origin: 50% 50%; animation: spin .8s linear infinite; }
|
||||
/* what has to listen at an address no agent reaches magpie by (#1013): long addresses wrap, and it can be copied */
|
||||
.drift-why { overflow-wrap: anywhere; user-select: text; -webkit-user-select: text; }
|
||||
/* the agent's CLI on its name's line (#202): its version faint, and a quiet
|
||||
@@ -425,8 +428,8 @@ body.window .row.agent .who .ag-cli { flex: 0 1 auto; overflow: hidden; }
|
||||
.ag-up:focus-visible { outline: 1.5px solid var(--fg-2); outline-offset: 1px; }
|
||||
.ag-up svg { color: var(--muted); }
|
||||
.ag-up.busy { pointer-events: none; color: var(--muted); }
|
||||
.ag-up.busy svg { animation: spin .8s linear infinite; }
|
||||
@media (prefers-reduced-motion: reduce) { .ag-up.busy svg { animation: none; } }
|
||||
.ag-up.busy svg > * { transform-box: view-box; transform-origin: 50% 50%; animation: spin .8s linear infinite; }
|
||||
@media (prefers-reduced-motion: reduce) { .ag-up.busy svg > * { animation: none; } }
|
||||
/* Update all (#727): a line above the list once two or more agents' CLIs
|
||||
are behind, its pill like a row's */
|
||||
.ag-updates { flex: none; display: flex; align-items: center; gap: 10px; margin: 0 2px 10px; min-height: 22px; font-size: 12.5px; color: var(--fg-2); }
|
||||
@@ -718,7 +721,7 @@ body.window .field.solo { grid-column: auto; }
|
||||
.quota-refresh:focus-visible { width: 14px; margin: 0 3px 0 -2px; opacity: 1; }
|
||||
.quota-refresh:hover { color: var(--fg); background: var(--pill-hover); }
|
||||
.quota-refresh.busy { cursor: default; }
|
||||
.quota-refresh.busy svg { animation: spin .8s linear infinite; }
|
||||
.quota-refresh.busy svg > * { transform-box: view-box; transform-origin: 50% 50%; animation: spin .8s linear infinite; }
|
||||
@media (hover: none) { .quota-refresh { width: 14px; margin: 0 3px 0 -2px; opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) { .quota-refresh { transition: none; } }
|
||||
.skeleton { display: block; border-radius: 5px; background: linear-gradient(90deg, var(--skel, var(--pill)) 25%, var(--skel-hi, var(--pill-hover)) 45%, var(--skel, var(--pill)) 65%); background-size: 240% 100%; animation: skeleton-shift 1.4s ease-in-out infinite; }
|
||||
@@ -2405,8 +2408,9 @@ body.window .ag-sk-icon { width: 22px; height: 22px; border-radius: 6px; }
|
||||
.editor .editor-error.warn { color: var(--amber); }
|
||||
.busy { opacity: .55; pointer-events: none; }
|
||||
.saving { opacity: 1; display: inline-flex; align-items: center; gap: 5px; }
|
||||
.saving svg { flex: none; animation: spin .8s linear infinite; }
|
||||
@media (prefers-reduced-motion: reduce) { .saving svg { animation: none; } }
|
||||
.saving svg { flex: none; }
|
||||
.saving svg > * { transform-box: view-box; transform-origin: 50% 50%; animation: spin .8s linear infinite; }
|
||||
@media (prefers-reduced-motion: reduce) { .saving svg > * { animation: none; } }
|
||||
|
||||
/* agent chips: who is on this provider, click one to pick it a model */
|
||||
.achips { display: flex; flex-wrap: wrap; gap: 5px; }
|
||||
@@ -3036,7 +3040,7 @@ body.panel .modal { padding: 16px 12px; }
|
||||
/* how often the Usage page reads its numbers again, and a way to read them now */
|
||||
.usage-refresh { display: inline-flex; align-items: center; gap: 2px; margin-right: 10px; }
|
||||
.usage-refresh .icon { width: 24px; height: 24px; }
|
||||
.usage-refresh .icon.busy svg { animation: spin .8s linear infinite; }
|
||||
.usage-refresh .icon.busy svg > * { transform-box: view-box; transform-origin: 50% 50%; animation: spin .8s linear infinite; }
|
||||
.usage-refresh .sess-pick { height: 24px; }
|
||||
.led-rank .rk .rk-b { overflow-wrap: anywhere; }
|
||||
|
||||
|
||||
@@ -20395,11 +20395,12 @@ $("#sync").onclick = async () => {
|
||||
// clock: waiting for animationiteration and then dropping the class
|
||||
// jumped the icon back by however far it had turned while the page was
|
||||
// busy drawing the reply — the twitch at the end of every refresh.
|
||||
const spin = b.querySelector("svg").getAnimations()[0];
|
||||
// The svg's drawing turns, not the svg (app.css), one animation a part.
|
||||
const spins = b.querySelector("svg").getAnimations({ subtree: true });
|
||||
const stop = () => b.classList.remove("spin");
|
||||
if (spin?.effect?.updateTiming) {
|
||||
spin.effect.updateTiming({ iterations: (spin.effect.getComputedTiming().currentIteration || 0) + 1 });
|
||||
spin.finished.then(stop, stop);
|
||||
if (spins.length && spins.every((a) => a.effect?.updateTiming)) {
|
||||
for (const a of spins) a.effect.updateTiming({ iterations: (a.effect.getComputedTiming().currentIteration || 0) + 1 });
|
||||
Promise.all(spins.map((a) => a.finished)).then(stop, stop);
|
||||
} else stop();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -130,6 +130,9 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
await page.waitForTimeout(150);
|
||||
assert.equal(await page.locator(`${row("codex")} .ag-up`).textContent(), "Updating…");
|
||||
assert.equal(await page.locator(`${row("codex")} .ag-up.busy`).count(), 1);
|
||||
// its ring turns inside the svg, round the viewBox's centre: a turned
|
||||
// svg circles about a point off its centre in WebKit (owner, 10-09)
|
||||
assert.deepEqual(await page.locator(`${row("codex")} .ag-up svg`).evaluate((s) => [getComputedStyle(s).animationName, getComputedStyle(s.firstElementChild).animationName, getComputedStyle(s.firstElementChild).transformBox]), ["none", "spin", "view-box"]);
|
||||
// clicked again while it runs: nothing more is asked
|
||||
await page.locator(`${row("codex")} .ag-up`).click({ force: true });
|
||||
await page.waitForTimeout(900);
|
||||
|
||||
@@ -78,7 +78,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
assert.equal(await busy.getAttribute("aria-busy"), "true");
|
||||
const look = await busy.evaluate((b) => {
|
||||
const s = getComputedStyle(b), svg = b.querySelector("svg"), r = b.getBoundingClientRect(), label = b.querySelector("span").getBoundingClientRect();
|
||||
return { opacity: s.opacity, events: s.pointerEvents, spin: getComputedStyle(svg).animationName, svgW: svg.getBoundingClientRect().width, inside: label.right <= r.right + 0.5 && label.width > 0, clipped: b.scrollWidth > b.clientWidth + 1 };
|
||||
return { opacity: s.opacity, events: s.pointerEvents, spin: getComputedStyle(svg.firstElementChild).animationName, svgW: svg.getBoundingClientRect().width, inside: label.right <= r.right + 0.5 && label.width > 0, clipped: b.scrollWidth > b.clientWidth + 1 };
|
||||
});
|
||||
assert.equal(look.opacity, "1", "readable, not faded as if off");
|
||||
assert.equal(look.events, "none", "not clicked twice");
|
||||
|
||||
@@ -61,7 +61,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
return remove.apply(this, names);
|
||||
};
|
||||
b.click();
|
||||
spin = svg.getAnimations()[0];
|
||||
spin = svg.getAnimations({ subtree: true })[0];
|
||||
}));
|
||||
assert.equal(end.state, "finished", `the spin was cut off mid-turn at ${Math.round(end.time)}ms`);
|
||||
assert(Number.isFinite(end.end) && Math.round(end.end) % 900 === 0, `the spin ends on a whole turn, not at ${end.end}ms`);
|
||||
|
||||
Reference in new issue
Block a user