From dbe2f01e7c9dd41a7fb355ba4e47b907d8df1f25 Mon Sep 17 00:00:00 2001 From: yetone Date: Tue, 29 Sep 2026 18:10:56 +0800 Subject: [PATCH] =?UTF-8?q?routing:=20the=20requests=20sit=20right=20under?= =?UTF-8?q?=20the=20stage,=20sized=20to=20stay=20in=20sight=20with=20it=20?= =?UTF-8?q?(Image=20#23:=20=E8=BF=99=E4=B8=80=E5=9D=97=E7=A6=BB=E9=82=A3?= =?UTF-8?q?=E4=B8=AA=E8=B7=AF=E7=94=B1=E5=8A=A8=E7=94=BB=E8=B7=9D=E7=A6=BB?= =?UTF-8?q?=E5=A4=AA=E8=BF=9C=E4=BA=86=EF=BC=8C=E7=94=A8=E6=88=B7=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E4=BA=86=E8=BF=99=E9=87=8C=E9=83=BD=E4=B8=8D=E7=9F=A5?= =?UTF-8?q?=E9=81=93=E5=8F=91=E7=94=9F=E4=BA=86=E4=BB=80=E4=B9=88)=20?= =?UTF-8?q?=E2=80=94=20the=20groups=20and=20the=20providers=20with=20sever?= =?UTF-8?q?al=20accounts=20or=20keys=20came=20between=20them,=20so=20a=20r?= =?UTF-8?q?equest=20picked=20in=20the=20list=20played=20on=20a=20stage=20s?= =?UTF-8?q?crolled=20out=20of=20sight=20above.=20They=20now=20come=20after?= =?UTF-8?q?=20the=20requests,=20and=20the=20list=20is=20as=20tall=20as=20l?= =?UTF-8?q?eaves=20the=20stage=20in=20view=20above=20it=20(216=E2=80=93420?= =?UTF-8?q?px),=20scrolling=20on=20its=20own=20rather=20than=20the=20page.?= =?UTF-8?q?=20Picking=20a=20request=20no=20longer=20sends=20the=20list=20b?= =?UTF-8?q?ack=20to=20its=20top=20in=20WebKit=20(it=20was=20emptied=20and?= =?UTF-8?q?=20refilled,=20which=20reset=20its=20scroll).=20click-scroll.te?= =?UTF-8?q?st.cjs=20brings=20a=20row=20into=20the=20list=20before=20pickin?= =?UTF-8?q?g=20it,=20as=20a=20reader=20would;=20Chromium=20and=20WebKit=20?= =?UTF-8?q?pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gui/assets/routing.css | 2 +- internal/gui/assets/routing.js | 21 ++++++++++++++++++++- internal/gui/tests/click-scroll.test.cjs | 11 +++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/internal/gui/assets/routing.css b/internal/gui/assets/routing.css index 9b557f75..448053b6 100644 --- a/internal/gui/assets/routing.css +++ b/internal/gui/assets/routing.css @@ -201,7 +201,7 @@ .rt-livedot { width: 6px; height: 6px; margin-left: 7px; border-radius: 50%; background: var(--green); animation: rtlive 1.8s infinite; --fg: var(--green); } @media (prefers-reduced-motion: reduce) { .rt-livedot { animation: none; } } /* routing groups, and the providers routing over several of their own */ -.rt-gsec { display: grid; gap: 6px; margin-bottom: 16px; } +.rt-gsec { display: grid; gap: 6px; margin-top: 16px; } .rt-gsec .row-head .note { margin-right: 6px; } .rt-gsec .rt-pools { margin-bottom: 0; } .rt-gsec .row-head + .list + .row-head { margin-top: 8px; } diff --git a/internal/gui/assets/routing.js b/internal/gui/assets/routing.js index f51c7b88..372f9388 100644 --- a/internal/gui/assets/routing.js +++ b/internal/gui/assets/routing.js @@ -1023,6 +1023,7 @@ reqNote.textContent = day ? t(pastCut ? "the last {n} of {day}" : "{n} on {day}", { n: rs.length, day: dayName(day) }) : t("the last {n} the gateway keeps", { n: rs.length }); renderDays(); + if (!reqs.style.maxHeight) requestAnimationFrame(fitReqs); // first shown // none yet: what the list is for in its place, and no accounts column // to tally nothing const none = !rs.length; @@ -1039,6 +1040,9 @@ renderActs(rs); return; } + // the list scrolls on its own; WebKit, emptied for a moment, would + // send it back to its top from under the row just picked + const listTop = reqs.scrollTop; reqs.replaceChildren(...rs.map((r) => { const [said, how, tr] = outcome(r); const b = el("button", "rt-req " + how); @@ -1071,6 +1075,7 @@ b.onclick = () => pick(r); return b; })); + if (reqs.scrollTop !== listTop) reqs.scrollTop = listTop; renderActs(rs); } @@ -1631,7 +1636,9 @@ const gHead = el("div", "row-head"), gList = el("div", "list rt-groups"); const pHead = el("div", "row-head"), pList = el("div", "list rt-pools"); gsec.append(gHead, gList, pHead, pList); - more.prepend(gsec); + // after the requests: a request picked in the list plays on the stage, + // so the list sits right under it + more.append(gsec); const ROUTE_OPTS = [["", "Smart"], ["order", "In order"], ["rotate", "In turn"], ["usage", "Least used"]]; const AFF_OPTS = [["", "Auto"], ["session", "Session"], ["turn", "Within a turn"], ["off", "Off"]]; const AFF_HINT = { @@ -2066,6 +2073,18 @@ loadGroups(); new ResizeObserver(() => layout()).observe(stage); + // the list is as tall as leaves the stage in sight above it: the reader + // scrolls the list, not the page, and a request picked plays in view. + // Its top stays where it is as it is sized, so a row just clicked does too + function fitReqs() { + const v = $("#view-routing"); + if (v.hidden || !reqs.offsetParent) return; + const above = reqs.getBoundingClientRect().top - box.getBoundingClientRect().top; + const room = v.clientHeight - above - 28; + reqs.style.maxHeight = Math.round(Math.max(216, Math.min(420, room))) + "px"; + } + new ResizeObserver(fitReqs).observe($("#view-routing")); + new ResizeObserver(fitReqs).observe(box); words(); requestAnimationFrame(frame); poll(); diff --git a/internal/gui/tests/click-scroll.test.cjs b/internal/gui/tests/click-scroll.test.cjs index 6b9e2235..3c19c103 100644 --- a/internal/gui/tests/click-scroll.test.cjs +++ b/internal/gui/tests/click-scroll.test.cjs @@ -107,6 +107,17 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", for (const i of [3, 7, 5]) { const row = () => page.locator(".rt-req").nth(i); await scrollTo(`.rt-req >> nth=${i}`); + // the list scrolls on its own: the reader brings the row into it + // (the click would, before it is measured) + const list = await page.locator(".rt-reqs").boundingBox(); + await page.mouse.move(list.x + list.width / 2, list.y + 20); + for (let k = 0; k < 40; k++) { + const b = await row().boundingBox(); + if (b.y + b.height <= list.y + list.height && b.y >= list.y) break; + await page.mouse.wheel(0, b.y < list.y ? -20 : 20); + await page.waitForTimeout(20); + } + await page.waitForTimeout(300); const was = await row().evaluate((e) => e.getBoundingClientRect().top); await row().click(); await settle(page);