gui: styles hold up in Safari 15.0–15.3 again (macOS 12, #220) — no :has(), and :focus-visible kept out of selector lists

old-webkit failed on main: app.css and routing.css had :focus-visible inside
selector lists (.quota-refresh, .sess-go, .acap, .alane, .us-handle, the
routing group and member grips) and inside :is() (.am-or's grip). Safari
before 15.4 drops the whole rule, so the hover reveal went too. Those parts
now sit in rules of their own. library.css and routing.css used :has().
A library row with agent chips is now marked .with-agents by agentChips,
and a request whose tag says where it went upstream is marked .upstreamed.

The keys-row collapse (#841) and the library chips' rings kept a control
out with :not(:focus-visible), which Safari before 15.4 also drops whole.
Each now has an @supports not selector(:focus-visible) copy on :focus.
old-webkit's checker now splits a selector list on its own commas, not
those inside :is()/:not(). It also wants that fallback wherever
:not(...:focus-visible...) is used.

The new old-webkit fails on origin/main's assets (12 findings) and passes
with this change in Chromium and WebKit. account-cap, lane-limit,
fold-show-grip, group-member-drag, library-all, library-items, pick-skills,
skill-name-many-agents, routing-upstream, usage-arrange, usage-card-refresh
and click-scroll pass in both. group-arrange fails in Chromium en the same
way on origin/main.
This commit is contained in:
yetone committed 2026-10-06 05:07:13 +08:00
1 parent a3726f657e
commit 69665a6960
6 files changed
+65 -18

No files matched your search

+30 -10
View File
@@ -695,7 +695,9 @@ body.window .field.solo { grid-column: auto; }
card's other lines do (#840) */
.quota-refresh { display: inline-flex; align-items: center; justify-content: center; width: 0; height: 14px; margin: 0; padding: 0; border: 0; border-radius: 4px; background: none; color: var(--faint); vertical-align: -3px; opacity: 0; overflow: hidden; cursor: pointer; transition: width .12s, margin .12s, opacity .12s; }
.quota-refresh svg { flex: none; }
.subscription-card:hover .quota-refresh, .quota-refresh:focus-visible, .quota-refresh.busy { width: 14px; margin: 0 3px 0 -2px; opacity: 1; }
.subscription-card:hover .quota-refresh, .quota-refresh.busy { width: 14px; margin: 0 3px 0 -2px; opacity: 1; }
/* apart: Safari before 15.4 drops a whole selector list with :focus-visible in it */
.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; }
@@ -1126,7 +1128,9 @@ details.cx-part[open] > summary .cx-h { margin-bottom: 2px; }
/* a row of the Usage page's list opens on the Sessions page (#752) */
.row.sess.sess-link { cursor: pointer; }
.row.sess .sess-go { flex: none; display: inline-flex; color: var(--faint); }
.row.sess.sess-link:hover .sess-go, .row.sess.sess-link:focus-visible .sess-go { color: var(--fg-2); }
.row.sess.sess-link:hover .sess-go { color: var(--fg-2); }
/* apart: Safari before 15.4 drops a whole selector list with :focus-visible in it */
.row.sess.sess-link:focus-visible .sess-go { color: var(--fg-2); }
.sess-all { flex: none; white-space: nowrap; }
.sess-resume { flex: none; height: 24px; padding: 0 10px; border-radius: 7px; border: 0; background: var(--pill); color: var(--fg); font-size: 11.5px; font-weight: 500; white-space: nowrap; }
.sess-resume:hover { background: var(--pill-hover); }
@@ -1874,14 +1878,18 @@ body.window .ag-sk-icon { width: 22px; height: 22px; border-radius: 6px; }
.acc .amodels:hover, .acc .amodels.open { color: var(--fg); background: var(--pill-hover); }
/* an account's usage cap: as the models pill, shown on hover till one is set */
.acc .acap { flex: none; display: inline-flex; align-items: center; height: 20px; padding: 0 7px; font: inherit; font-size: 11px; color: var(--muted); border: 0; border-radius: 5px; background: none; box-shadow: inset 0 0 0 1px var(--line); white-space: nowrap; cursor: default; opacity: 0; transition: opacity .15s, background .12s, color .12s; }
.acc:hover .acap, .acc .acap.open, .acc .acap:focus-visible, .acc .acap.set { opacity: 1; }
.acc:hover .acap, .acc .acap.open, .acc .acap.set { opacity: 1; }
/* apart: Safari before 15.4 drops a whole selector list with :focus-visible in it */
.acc .acap:focus-visible { opacity: 1; }
.acc .acap.set { color: var(--accent); background: var(--accent-soft); box-shadow: none; font-weight: 500; }
.acc .acap:hover, .acc .acap.open { color: var(--fg); background: var(--pill-hover); }
.acc .acap-in { width: 64px; }
/* an account's or key's limit on requests at once (#892), and how many
run and wait under it: shown on hover unless a limit applies */
.acc .alane { flex: none; display: inline-flex; align-items: center; height: 20px; padding: 0 7px; font: inherit; font-size: 11px; color: var(--muted); border: 0; border-radius: 5px; background: none; box-shadow: inset 0 0 0 1px var(--line); white-space: nowrap; cursor: default; opacity: 0; transition: opacity .15s, background .12s, color .12s; font-variant-numeric: tabular-nums; }
.acc:hover .alane, .acc .alane.open, .acc .alane:focus-visible, .acc .alane.set { opacity: 1; }
.acc:hover .alane, .acc .alane.open, .acc .alane.set { opacity: 1; }
/* apart: Safari before 15.4 drops a whole selector list with :focus-visible in it */
.acc .alane:focus-visible { opacity: 1; }
.acc .alane.set { color: var(--fg); background: var(--pill-hover); box-shadow: none; }
.acc .alane.busy-lane { color: var(--accent); background: var(--accent-soft); font-weight: 500; }
.acc .alane.queued { color: var(--amber); }
@@ -1903,14 +1911,22 @@ body.window .ag-sk-icon { width: 22px; height: 22px; border-radius: 6px; }
badge gives its own up first. First and Make first stay one line */
.accts.keys .acc .plan { flex: 0 1 auto; min-width: 3em; overflow: hidden; text-overflow: ellipsis; }
.accts.keys .acc > :is(.amodels:not(.set), .proto:not(.set), .alane:not(.set), .text.quiet) { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.accts.keys .acc:not(:hover) > :is(.amodels:not(.set), .proto:not(.set), .alane:not(.set), .text.quiet):not(:focus-visible, .open) { max-width: 0; padding-left: 0; padding-right: 0; box-shadow: none; }
.accts.keys .acc > :is(.amodels, .alane):not(.set):not(:focus-visible, .open) { flex-shrink: 10000; }
.accts.keys .acc:not(:hover) > :is(.amodels:not(.set), .proto:not(.set), .alane:not(.set), .text.quiet):not(.open):not(:focus-visible) { max-width: 0; padding-left: 0; padding-right: 0; box-shadow: none; }
.accts.keys .acc > :is(.amodels, .alane):not(.set):not(.open):not(:focus-visible) { flex-shrink: 10000; }
/* the limit pill unset on a key's row is quiet text, as the row's other
hover-only controls: no padding to keep, and hidden it takes back the
gap before it too (#892) */
.accts.keys .acc > .alane:not(.set):not(:focus-visible, .open) { padding: 0; box-shadow: none; background: none; }
.accts.keys .acc:not(:hover) > .alane:not(.set):not(:focus-visible, .open) { margin-left: -9px; }
.accts.keys .acc > .alane:not(.set):not(.open):not(:focus-visible) { padding: 0; box-shadow: none; background: none; }
.accts.keys .acc:not(:hover) > .alane:not(.set):not(.open):not(:focus-visible) { margin-left: -9px; }
.accts.keys .acc > :is(.proto, .text.quiet) { flex-shrink: 0; }
/* Safari before 15.4 has no :focus-visible and drops the rules above
whole: there a focused control stays out on :focus instead */
@supports not selector(:focus-visible) {
.accts.keys .acc:not(:hover) > :is(.amodels:not(.set), .proto:not(.set), .alane:not(.set), .text.quiet):not(.open):not(:focus) { max-width: 0; padding-left: 0; padding-right: 0; box-shadow: none; }
.accts.keys .acc > :is(.amodels, .alane):not(.set):not(.open):not(:focus) { flex-shrink: 10000; }
.accts.keys .acc > .alane:not(.set):not(.open):not(:focus) { padding: 0; box-shadow: none; background: none; }
.accts.keys .acc:not(:hover) > .alane:not(.set):not(.open):not(:focus) { margin-left: -9px; }
}
.accts.keys .acc .using, .accts.keys .acc > .text:not(.quiet) { flex: none; white-space: nowrap; }
#gatewayKeys .acc { min-width: 0; }
#gatewayKeys .acc .plan { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
@@ -2714,7 +2730,9 @@ body.panel .modal { padding: 16px 12px; }
with 3.5px between them the right column was cut in half (Hu9956, #842) */
.subscription-head .us-handle { position: relative; width: 24px; height: 24px; margin: -4px -3px; }
.subscription-head .us-handle::before { content: ""; position: absolute; left: -8px; top: 50%; width: 6px; height: 11px; margin-top: -5.5px; background: radial-gradient(circle, var(--muted) .9px, transparent 1.2px) 0 0 / 3px 3.75px; opacity: 0; transition: opacity .15s; }
.subscription-card:is(:hover, .dragging) .us-handle::before, .us-handle:focus-visible::before { opacity: 1; }
.subscription-card:is(:hover, .dragging) .us-handle::before { opacity: 1; }
/* apart: Safari before 15.4 drops a whole selector list with :focus-visible in it */
.us-handle:focus-visible::before { opacity: 1; }
.subscription-usage.sorting { user-select: none; -webkit-user-select: none; }
.subscription-usage.sorting > .subscription-card { transition: transform .22s cubic-bezier(.22, 1, .36, 1); }
.subscription-usage.sorting .us-handle { cursor: grabbing; }
@@ -2904,7 +2922,9 @@ body.window .row.agent .who.with-models { flex-wrap: wrap; row-gap: 0; }
.am-order { padding-top: 4px; }
.am-rows.am-order .am-mr { padding-left: 14px; cursor: grab; user-select: none; -webkit-user-select: none; }
.am-or .grip { flex: none; width: 6px; height: 11px; margin-right: -4px; background: radial-gradient(circle, var(--muted) .9px, transparent 1.2px) 0 0 / 3px 3.75px; opacity: 0; transition: opacity .15s; }
.am-or:is(:hover, :focus-visible, .dragging) .grip { opacity: 1; }
.am-or:is(:hover, .dragging) .grip { opacity: 1; }
/* apart: Safari before 15.4 drops a whole selector list with :focus-visible in it */
.am-or:focus-visible .grip { opacity: 1; }
@media (hover: none) { .am-or .grip { opacity: 1; } }
.am-or .ix { flex: none; min-width: 16px; font-size: 11.5px; color: var(--faint); font-variant-numeric: tabular-nums; text-align: right; }
.am-order.sorting .am-or { transition: transform .22s cubic-bezier(.22, 1, .36, 1); cursor: grabbing; }
+12 -2
View File
@@ -74,7 +74,7 @@ body.window .lib-head { top: -2px; margin: -2px -16px 0; padding: 8px 16px 4px;
the row wider than the list (#257) */
.lib-row .who { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
/* and beside a hand of many agents' chips it keeps room to be read (#936) */
.lib-row:has(> .lib-agents) > .who { min-width: min(140px, 32%); }
.lib-row.with-agents > .who { min-width: min(140px, 32%); }
/* a server's or skill's picture sits level with the top of its name */
.lib-row > .lib-logo, .lib-row > .lib-glyph { align-self: flex-start; margin-top: 1px; }
.lib-row .name.mono, .lib-pick .name.mono { font: 500 12.5px var(--code); }
@@ -217,7 +217,7 @@ input.lib-setname:focus { border-color: var(--accent); box-shadow: 0 0 0 3px col
the spacer before them is the difference. A WebKit without container
units keeps -7px and 3px. */
@supports (width: 1cqw) {
.lib-row:has(> .lib-agents) { container-type: inline-size; }
.lib-row.with-agents { container-type: inline-size; }
.lib-row > .lib-agents {
--lib-n: var(--lib-agent-count, 1);
--lib-gap: calc((48cqw - 60px - var(--lib-n) * 24px) / max(1, var(--lib-n) - 1));
@@ -524,3 +524,13 @@ input.lib-setname:focus { border-color: var(--accent); box-shadow: 0 0 0 3px col
.lib-subhead > .lib-agents { margin-left: auto; }
.lib-pickbar .lib-groupname { flex: 1 1 140px; min-width: 100px; max-width: 240px; margin: 0; }
.lib-grouphead .lib-grouprename-in { height: 24px; margin: 0; max-width: 260px; font-weight: 500; }
/* Safari before 15.4 has no :focus-visible and drops the chips' rings
above: there they go by :focus */
@supports not selector(:focus-visible) {
.lib-row > .lib-agents .lib-ag:not(:focus) { outline: 1.5px solid var(--card); outline-offset: 0; }
.lib-row > .lib-agents:hover .lib-ag:not(:focus), .lib-row > .lib-agents:focus-within .lib-ag:not(:focus) { outline-color: transparent; }
.lib-row.click:hover > .lib-agents .lib-ag:not(:focus) { outline-color: var(--card-2); }
.lib-row.click:hover > .lib-agents:hover .lib-ag:not(:focus) { outline-color: transparent; }
.lib-grouphead > .lib-agents .lib-ag:not(:focus) { outline-color: var(--card-2); }
}
+3
View File
@@ -223,6 +223,9 @@
on ||= [];
const box = el("div", "lib-agents");
box.style.setProperty("--lib-agent-count", all.length);
// the row it is put in says so (.with-agents), for its name's room:
// Safari before 15.4 has no :has(). Every caller appends it at once.
queueMicrotask(() => box.parentElement?.classList.contains("lib-row") && box.parentElement.classList.add("with-agents"));
for (const a of all) {
const always = opts.always?.(a) || "";
const has = on.includes(a.id) || !!always;
+9 -4
View File
@@ -201,8 +201,9 @@
.rt-req .to .swap { flex: none; max-width: 70%; }
.rt-req .to .routed, .pr-to .routed { flex: none; max-width: 45%; overflow: hidden; text-overflow: ellipsis; font-size: 10.5px; line-height: 16px; color: var(--muted); white-space: nowrap; }
.rt-steps li.swap::before { background: var(--amber); }
/* short of room, where it went gives way down to the provider's name, then this does */
.rt-req .to:has(.upstream) .said { min-width: min(10em, 45%); }
/* short of room, where it went gives way down to the provider's name, then this does
(.upstreamed, set with the tag: Safari before 15.4 has no :has()) */
.rt-req .to.upstreamed .said { min-width: min(10em, 45%); }
.rt-req .to .upstream, .pr-to .upstream { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 10.5px; line-height: 16px; color: var(--muted); white-space: nowrap; }
.rt-req .meta { display: flex; align-items: baseline; justify-content: flex-end; flex-wrap: wrap; gap: 2px 0; color: var(--muted); font-variant-numeric: tabular-nums; }
.rt-metric { display: inline-flex; align-items: baseline; gap: 4px; white-space: nowrap; min-width: 0; }
@@ -310,7 +311,9 @@ button.rt-session { cursor: pointer; }
on Discord) */
.rt-group .rt-ghandle.ag-handle { position: relative; align-items: center; justify-content: center; width: auto; min-width: 24px; height: 24px; padding: 0 3px; }
.rt-group .rt-ghandle::before { content: ""; position: absolute; left: -8px; top: 50%; width: 5px; height: 11px; margin-top: -5.5px; background: radial-gradient(circle, var(--muted) .9px, transparent 1.2px) 0 0 / 3.5px 3.75px; opacity: 0; transition: opacity .15s; }
.rt-group:is(:hover, .dragging) .rt-ghandle::before, .rt-group .rt-ghandle.open::before, .rt-group .rt-ghandle:focus-visible::before { opacity: 1; }
.rt-group:is(:hover, .dragging) .rt-ghandle::before, .rt-group .rt-ghandle.open::before { opacity: 1; }
/* apart: Safari before 15.4 drops a whole selector list with :focus-visible in it */
.rt-group .rt-ghandle:focus-visible::before { opacity: 1; }
/* while a row is dragged the others slide out of its way */
.rt-groups.sorting { user-select: none; -webkit-user-select: none; }
.rt-groups.sorting > .rt-group { transition: transform .22s cubic-bezier(.22, 1, .36, 1); }
@@ -420,7 +423,9 @@ button.rt-session { cursor: pointer; }
hover, dragged or from the keyboard only, as on the groups' cards */
.rt-gedit .fbrow .rt-mhandle.ag-handle { position: relative; width: 18px; height: 22px; margin: 0 -2px; font: inherit; color: var(--muted); font-variant-numeric: tabular-nums; }
.rt-gedit .fbrow .rt-mhandle::before { content: ""; position: absolute; left: -7px; top: 50%; width: 5px; height: 11px; margin-top: -5.5px; background: radial-gradient(circle, var(--muted) .9px, transparent 1.2px) 0 0 / 3.5px 3.75px; opacity: 0; transition: opacity .15s; }
.rt-gedit .fbrow:is(:hover, .dragging) .rt-mhandle::before, .rt-gedit .fbrow .rt-mhandle:focus-visible::before { opacity: 1; }
.rt-gedit .fbrow:is(:hover, .dragging) .rt-mhandle::before { opacity: 1; }
/* apart: Safari before 15.4 drops a whole selector list with :focus-visible in it */
.rt-gedit .fbrow .rt-mhandle:focus-visible::before { opacity: 1; }
.rt-gedit .fbl.sorting { user-select: none; -webkit-user-select: none; }
.rt-gedit .fbl.sorting > .fbrow { transition: transform .22s cubic-bezier(.22, 1, .36, 1); }
.rt-gedit .fbl.sorting .rt-mhandle { cursor: grabbing; }
+1 -1
View File
@@ -1757,7 +1757,7 @@
}
if (tr?.swapped && tr.done && tryOk(tr)) to.append(swapTag(tr, true)); // beside the model asked for
else if (tr?.routed && tr.done && tryOk(tr)) to.append(routedTag(tr));
if (tr?.upstream && tr.done && tryOk(tr)) to.append(upstreamTag(tr));
if (tr?.upstream && tr.done && tryOk(tr)) { to.append(upstreamTag(tr)); to.classList.add("upstreamed"); }
const info = el("span", "meta");
if (meta.length) info.append(el("span", "retry-count", meta.join(" · ")));
info.append(...metrics.map((m) => metricElement(m, true)));
+10 -1
View File
@@ -155,10 +155,19 @@ test("the styles hold up in Safari 15.0", async () => {
for (const m of css.matchAll(/([^{}]+)\{/g)) {
const sel = m[1].trim();
if (sel.startsWith("@")) continue;
const parts = sel.split(",");
// the list's own commas, not those inside :is() or :not()
const parts = []; let depth = 0, from = 0;
for (let i = 0; i < sel.length; i++) {
if (sel[i] === "(") depth++;
else if (sel[i] === ")") depth--;
else if (sel[i] === "," && !depth) { parts.push(sel.slice(from, i)); from = i + 1; }
}
parts.push(sel.slice(from));
if (parts.some((p) => p.includes(":focus-visible")) && !parts.every((p) => p.includes(":focus-visible"))) bad.push(`${f}:~${lineOf(m.index)} :focus-visible in a selector list (Safari 15.4 drops the rule): ${sel.slice(0, 90)}`);
if (/:is\([^)]*:focus-visible/.test(sel)) bad.push(`${f}:~${lineOf(m.index)} :focus-visible inside :is(): ${sel.slice(0, 90)}`);
}
// a rule kept off a focused control drops whole there: one for :focus beside it
if (/:not\([^)]*:focus-visible/.test(css) && !/@supports not selector\(:focus-visible\)/.test(css)) bad.push(`${f}: :not(:focus-visible) (Safari 15.4) with no @supports not selector(:focus-visible) fallback`);
if (/@container\b/.test(css) && !/@supports not \(container-type: inline-size\)/.test(css)) bad.push(`${f}: @container (Safari 16) with no @supports not (container-type: inline-size) fallback`);
if (/:\s*subgrid\b/.test(css) && !/@supports not \(grid-template-columns: subgrid\)/.test(css)) {
// a subgrid that only lines rows up across cards may go without