Skip to content

Commit 068346b

Browse files
committed
Fix mobile topbar scroll stability
1 parent 77c8bbb commit 068346b

3 files changed

Lines changed: 39 additions & 43 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
- The Web UI check script auto-scans Web UI `src/` and `scripts/`. Keep it as the single package-level entrypoint instead of lengthening `package.json` with per-file `node --check` lists.
9595
- Segmented controls should share the established title-bar pill geometry: real capsule ends (`border-radius: 999px` and `corner-shape: round` where used), inset-shadow borders instead of layout-affecting borders, consistent inner inset, and draggable thumb support when the paired control supports it. Keep click and drag paths separate so click transitions can animate and drag can follow the pointer.
9696
- For touch UI, do not rely on mobile Safari clearing `:hover`. Gate hover-only highlights behind `(hover: hover) and (pointer: fine)`, and clear touch active classes on `pointerup`, `pointercancel`, and blur.
97+
- For touch/mobile topbar stability, keep the fixed header path free of expensive compositor triggers such as `contain: paint`, forced transforms, `will-change`, and backdrop blur; iOS Safari can hide or jitter fixed headers during address-bar scroll when those are stacked together.
9798
- iOS/iPadOS file pickers can gray out `.apk/.apks/.apkm/.xapk` when `accept` is set. Use the existing `file-picker-support.js` path to relax picker filtering on Apple mobile WebKit while preserving in-app file validation.
9899
- Liquid/glass-like effects must be capability- and power-mode gated, not OS-name gated. `CSS.supports()` can be a false positive for SVG-backed backdrop filters on Android/iOS browser shells; keep explicit fallback reasons in `liquid-glass-support.js` and prefer old stable backgrounds when rendering is suspect.
99100
- WebUI CSS gzip budget is intentionally tight. Before raising the budget, prefer lazy CSS for non-first-screen UI (`runtime-log.css`, `lcapps-bubble.css`), removing duplicated control rules, or consolidating existing tokens. A modest budget raise is acceptable only when the transferred gzip size and first-screen impact are understood.

packages/apk-webui/src/app.css

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@ body {
183183
color: var(--text);
184184
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", "PingFang SC", "Segoe UI", sans-serif;
185185
-webkit-font-smoothing: antialiased;
186-
text-rendering: optimizeLegibility;
187186
-webkit-touch-callout: none;
188187
-webkit-user-select: none;
189188
user-select: none;
@@ -345,7 +344,6 @@ svg {
345344
overflow: hidden;
346345
overflow: clip;
347346
contain: paint;
348-
backface-visibility: hidden;
349347
will-change: transform;
350348
transform: translate3d(0, 0, 0);
351349
}
@@ -359,7 +357,6 @@ svg {
359357
background: var(--topbar-bg);
360358
-webkit-backdrop-filter: saturate(180%) blur(18px);
361359
backdrop-filter: saturate(180%) blur(18px);
362-
backface-visibility: hidden;
363360
will-change: transform, backdrop-filter;
364361
transform: translate3d(0, 0, 0);
365362
}
@@ -393,7 +390,6 @@ svg {
393390
width: max-content;
394391
max-width: var(--brand-title-max-width);
395392
min-width: 0;
396-
overflow: visible;
397393
}
398394

399395
.brand-mark {
@@ -403,7 +399,6 @@ svg {
403399
place-items: center;
404400
width: 26px;
405401
height: 26px;
406-
overflow: visible;
407402
}
408403

409404
.brand-orb {
@@ -495,15 +490,10 @@ svg {
495490
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Segoe UI", sans-serif;
496491
font-size: 14px;
497492
font-weight: 600;
498-
font-kerning: normal;
499493
line-height: 1.15;
500494
letter-spacing: 0;
501495
white-space: nowrap;
502496
max-width: var(--brand-title-max-width);
503-
cursor: default;
504-
-webkit-touch-callout: none;
505-
-webkit-user-select: none;
506-
user-select: none;
507497
}
508498

509499
.brand-title.is-text-rendered {
@@ -515,10 +505,6 @@ svg {
515505
line-height: 1;
516506
}
517507

518-
.brand-title.is-switching-title {
519-
overflow: visible;
520-
}
521-
522508
.brand-title__layer {
523509
grid-area: 1 / 1;
524510
display: block;
@@ -527,9 +513,6 @@ svg {
527513
overflow: hidden;
528514
text-overflow: ellipsis;
529515
white-space: nowrap;
530-
cursor: default;
531-
-webkit-user-select: none;
532-
user-select: none;
533516
}
534517

535518
.brand-title__text {
@@ -556,7 +539,6 @@ svg {
556539
overflow: visible;
557540
padding: 0 12px;
558541
margin: 0 -12px;
559-
text-overflow: clip;
560542
}
561543

562544
.brand-title__layer--enter .brand-title__blur-word {
@@ -584,7 +566,6 @@ svg {
584566
overflow: hidden;
585567
padding: 0 12px;
586568
margin: 0;
587-
text-overflow: clip;
588569
}
589570

590571
.brand-title__layer--exit .brand-title__blur-word {
@@ -764,14 +745,6 @@ svg {
764745
box-shadow: inset 0 0 0 1px var(--topbar-focus);
765746
}
766747

767-
.mode-chip.is-active {
768-
color: var(--topbar-selected-text);
769-
}
770-
771-
.mode-chip.is-pending {
772-
color: var(--topbar-selected-text);
773-
}
774-
775748
.topbar-github-link {
776749
flex: 0 0 auto;
777750
color: var(--topbar-muted);
@@ -863,11 +836,9 @@ svg {
863836
box-shadow: inset 0 0 0 1px var(--topbar-focus);
864837
}
865838

866-
.theme-chip.is-active {
867-
background: transparent;
868-
color: var(--topbar-selected-text);
869-
}
870-
839+
.mode-chip.is-active,
840+
.mode-chip.is-pending,
841+
.theme-chip.is-active,
871842
.theme-chip.is-pending {
872843
color: var(--topbar-selected-text);
873844
}
@@ -4409,11 +4380,6 @@ svg {
44094380
margin-right: 12px;
44104381
}
44114382

4412-
.brand-title.is-text-rendered,
4413-
.brand-title__layer {
4414-
flex-wrap: nowrap;
4415-
}
4416-
44174383
.topbar-actions {
44184384
flex: 1 1 auto;
44194385
width: auto;
@@ -4543,9 +4509,6 @@ svg {
45434509
@media (max-width: 600px) {
45444510
.brand {
45454511
max-width: min(32vw, 136px);
4546-
}
4547-
4548-
.brand {
45494512
--brand-title-max-width: clamp(72px, 18vw, 96px);
45504513
}
45514514
}
@@ -4697,6 +4660,25 @@ svg {
46974660

46984661
}
46994662

4663+
@media (hover: none), (pointer: coarse) {
4664+
.color-orb-background {
4665+
transform: none;
4666+
}
4667+
4668+
.topbar {
4669+
contain: none;
4670+
will-change: auto;
4671+
transform: none;
4672+
}
4673+
4674+
.topbar::before {
4675+
-webkit-backdrop-filter: none;
4676+
backdrop-filter: none;
4677+
will-change: auto;
4678+
transform: none;
4679+
}
4680+
}
4681+
47004682
@keyframes progress-shiny-text {
47014683
0% {
47024684
background-position: 150% center;

packages/apk-webui/src/app.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2509,6 +2509,20 @@ function initColorOrbBackground() {
25092509
const dpr = Math.min(window.devicePixelRatio || 1, DOT_FIELD_DPR_LIMIT);
25102510
const pixelWidth = Math.max(1, Math.round(width * dpr));
25112511
const pixelHeight = Math.max(1, Math.round(height * dpr));
2512+
const sizeChanged = (
2513+
metrics.width !== width ||
2514+
metrics.height !== height ||
2515+
metrics.dpr !== dpr ||
2516+
canvas.width !== pixelWidth ||
2517+
canvas.height !== pixelHeight
2518+
);
2519+
2520+
metrics.left = rect.left;
2521+
metrics.top = rect.top;
2522+
if (!sizeChanged) {
2523+
drawFrame();
2524+
return;
2525+
}
25122526

25132527
if (canvas.width !== pixelWidth) {
25142528
canvas.width = pixelWidth;
@@ -2519,8 +2533,6 @@ function initColorOrbBackground() {
25192533

25202534
metrics.width = width;
25212535
metrics.height = height;
2522-
metrics.left = rect.left;
2523-
metrics.top = rect.top;
25242536
metrics.dpr = dpr;
25252537
context.setTransform(dpr, 0, 0, dpr, 0, 0);
25262538
buildDots();
@@ -2770,12 +2782,13 @@ function initColorOrbBackground() {
27702782
window.addEventListener("blur", clearPointer);
27712783
window.addEventListener("apk-theme-change", refreshPalette);
27722784
window.addEventListener("apk-power-mode-change", refreshMotionMode);
2785+
addChangeListener(fineHoverMedia, refreshMotionMode);
27732786
document.addEventListener("visibilitychange", refreshMotionMode);
27742787
startAnimation();
27752788
}
27762789

27772790
function shouldAnimateDotField() {
2778-
return !isAppPowerConstrained() && document.visibilityState !== "hidden";
2791+
return !isAppPowerConstrained() && document.visibilityState !== "hidden" && fineHoverMedia.matches;
27792792
}
27802793

27812794
function resolveDotFieldPalette() {

0 commit comments

Comments
 (0)