From a553931c76ccc985eba48158f5bdf60da7975253 Mon Sep 17 00:00:00 2001 From: rays Date: Fri, 9 Oct 2026 00:50:28 +0000 Subject: [PATCH] Run the top bar's colour up behind the Dynamic Island on iOS 26 (#147) 46f9370 set theme-color, but Safari 26 ignores it. iOS 26 paints the strip behind the status bar and the island in the background colour of a sticky or fixed element at the page's top edge, and failing that the root's. The top bar was neither, and the root had no colour of its own (only body did), so iOS used black or white: a band of another colour above the bar. The bar is sticky now, though nothing scrolls under it, so iOS reads its colour; the root has the page's background for screens with nothing at the top edge; Classic's metal bar gets a colour under its gradient, the top of the metal, since iOS reads no gradient. theme-color stays, set to the bar's colour, for iPhones before iOS 26 and for Android. Glass's bar is see-through, so iOS frosts what is under it there, as the bar itself does. Taken from developers' reports of iOS 26, not Apple's documentation, and not yet seen on a phone. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- tests/ui/app.spec.js | 10 ++++++---- web/app.css | 10 ++++++++++ web/src/theme.ts | 7 ++++--- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6382391..66d7860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- On an iPhone, the bar around the Dynamic Island is the theme's background, not the system's own black or white. +- On an iPhone, the top bar's colour runs all the way up behind the status bar and the Dynamic Island, with no band of another colour above it. - A feed's buttons stay on one row on a narrow phone, and the reader's buttons stay together instead of breaking across lines under a long feed name. - A moment's DNS failure no longer marks a feed as failing for a day or calls it gone: iPX tries it again in ten minutes. One failed lookup one morning left 205 podcasts unread, uncategorised and labelled gone until the next day. A name that does not resolve is said to be maybe gone, and only once it has been failing a day, on its Directory page as on its own. diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index 5d404f9..306a8ba 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -1667,9 +1667,11 @@ test.describe('on a narrow phone', () => { const tops = await page.locator('#content .acts .btn').evaluateAll(bs => bs.map(b => b.getBoundingClientRect().top)); expect(tops.length).toBe(7); expect(new Set(tops).size).toBe(1); - // iOS paints the status bar round the Dynamic Island in theme-color, its own black or white - // without one (#147). - expect(await page.evaluate(() => document.querySelector('meta[name=theme-color]').content)) - .toBe(await page.evaluate(() => getComputedStyle(document.body).backgroundColor)); + // iOS 26 paints behind the status bar and the Dynamic Island in the colour of a sticky bar at + // the top edge, older ones in theme-color: both the top bar's, which is the page's (#147). + const bar = await page.locator('#topbar').evaluate(b => [getComputedStyle(b).position, b.getBoundingClientRect().top, getComputedStyle(b).backgroundColor]); + expect(bar.slice(0, 2)).toEqual(['sticky', 0]); + expect(bar[2]).toBe(await page.evaluate(() => getComputedStyle(document.body).backgroundColor)); + expect(await page.evaluate(() => document.querySelector('meta[name=theme-color]').content)).toBe(bar[2]); }); }); diff --git a/web/app.css b/web/app.css index 65b25fb..bf3ccf6 100644 --- a/web/app.css +++ b/web/app.css @@ -458,6 +458,10 @@ /* A rule that sets display beats the UA's [hidden], and several below do. */ [hidden]{display:none!important} html,body{height:100%} +/* What iOS paints behind the status bar where nothing sticky is at the top edge, as in the + reader on a phone, which brings its own. With no colour of its own, the root went black or + white there instead (#147). */ +html{background-color:var(--bg)} body{ margin:0;background:var(--bg);color:var(--fg); font:14.5px/1.55 Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; @@ -522,6 +526,10 @@ a{color:var(--accent)} padding-top:calc(7px + var(--safe-t));padding-bottom:7px; padding-left:calc(12px + var(--safe-l));padding-right:calc(12px + var(--safe-r)); background:var(--panel);border-bottom:1px solid var(--line); + /* Nothing scrolls under it: it is sticky only because iOS 26 paints the strip behind the status + bar and the Dynamic Island in the colour of a sticky or fixed bar at the top edge, and ignores + theme-color, so the bar now runs all the way up behind the island (#147). */ + position:sticky;top:0; } #topbar .grow{flex:1} #topbar #epSearch{width:260px;flex:none} @@ -1265,6 +1273,8 @@ body:has(#player.on) #status{padding-bottom:3px} :root[data-theme="classic"] #topbar, :root[data-theme="classic"] #status, :root[data-theme="classic"] #player{background:linear-gradient(#ececec,#c9c9c9);border-color:#8e8e8e;color:#1a1a1a} +/* The colour iOS takes for above the bar, where it reads no gradient: the top of the metal. */ +:root[data-theme="classic"] #topbar{background-color:#ececec} :root[data-theme="classic"] #status{text-align:center} :root[data-theme="classic"] #sidebar{background:#e7ebf1;border-right-color:#a3a3a3} :root[data-theme="classic"] #files{background:#f4f4f4} diff --git a/web/src/theme.ts b/web/src/theme.ts index 0e488b0..0d8d979 100644 --- a/web/src/theme.ts +++ b/web/src/theme.ts @@ -38,9 +38,10 @@ function setTheme(name = theme.name, mode = theme.mode, save = false){ : theme.mode === 'auto' ? (systemDark && !systemDark.matches ? 'light' : 'dark') : theme.mode; // The tab's icon in the same variant as the logo on the page. const fav = $('#favicon'); if(fav) fav.href = fav.dataset[root.dataset.mode]; - // The bar iOS draws round the Dynamic Island takes this colour, as Android's toolbar does. - // Without it the bar was the system's own black or white, a band across the top of the page. - const tc = $('meta[name=theme-color]'); if(tc) tc.content = getComputedStyle(document.body).backgroundColor; + // The top bar's colour for an iPhone before iOS 26 to paint behind the status bar, as Android's + // toolbar does; the page's where the bar is see-through (Glass). iOS 26 reads the bar itself. + const tc = $('meta[name=theme-color]'), bar = $('#topbar'); + if(tc){ const c = bar ? getComputedStyle(bar).backgroundColor : ''; tc.content = /^rgb\(/.test(c) ? c : getComputedStyle(root).backgroundColor; } const sel = $('#stheme'); if(sel) sel.value = theme.name; const ms = $('#smode'); if(ms) ms.value = theme.mode; const mf = $('#smodefield'); if(mf) mf.hidden = !both;