diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index 32d2e8d..6111626 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -1906,6 +1906,22 @@ test('a refresh button turns from the press to the end of the scan it asked for' expect(await spin('#scanAll .i')).toBe('none'); }); +test.describe('with motion reduced', () => { + test.use({ reducedMotion: 'reduce' }); + + test('a check running still shows, as a pulse and in its label', async ({ page }) => { + // Turning was all that showed it, and with motion reduced nothing turned (#310). + const all = page.locator('#scanAll'); + await page.evaluate(() => spinStart(null)); + await expect(all).toHaveAttribute('aria-busy', 'true'); + await expect(all).toHaveAttribute('aria-label', 'Checking for new items…'); + expect(await all.locator('.i').evaluate(el => getComputedStyle(el).animationName)).toBe('ipxpulse'); + await page.evaluate(() => spinEnd({ only: null, forced: true })); + await expect(all).not.toHaveAttribute('aria-busy'); + await expect(all).toHaveAttribute('aria-label', 'Check every feed for new items'); + }); +}); + test.describe('on a narrow phone', () => { test.use({ viewport: { width: 360, height: 760 } }); diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index b8f467f..d47e200 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -23,6 +23,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Text boxes, selects and switches have an edge you can see in every theme, and a box with the keyboard in it has the same ring as every other control (#305, #308). - Dialogs are dialogs to a screen reader, named by their heading. The page behind one is out of reach while it is open, and closing it puts the keyboard back where it was (#306). - The boxes in Add a feed, Settings, Download latest and OPML are named by their labels, and clicking a label puts the cursor in its box (#307). +- With reduced motion on, a check for new items still shows: the refresh button pulses and says "Checking for new items", spinners pulse, and toasts and sheets fade in (#310). +- On a phone, a tapped button, feed or Directory category no longer stays shaded or fanned out as if it were selected (#311). +- Scrolling to the end of a dialog or the phone's feed list no longer scrolls the page behind it (#312). +- Cover artwork has a faint edge, so a dark cover stands off a dark theme (#313). +- The tab bar's corners follow the tabs inside it (#314), and the Directory's covers fan out smoothly (#315). - Settings shows iPX's and the page's versions at its foot: the logo's tooltip was out of reach of a keyboard and of a phone (#309). ## [1.2.0] - 2026-10-19 diff --git a/web/app.css b/web/app.css index adca094..df9c26c 100644 --- a/web/app.css +++ b/web/app.css @@ -495,7 +495,15 @@ a{color:var(--accent)} .tgroup button:focus-visible,.tgroup a:focus-visible,.feed:focus-visible,.place:focus-visible,.chev:focus-visible{outline-offset:-2px} @media (prefers-reduced-motion:reduce){ *,*::before,*::after{animation:none!important;transition:none!important} + /* Movement goes, not what it said: a still ring read as nothing happening (#310). Spinners pulse + instead of turning, and toasts and sheets fade in instead of sliding. */ + .dlink.busy::after,.feed.scanning::after,.listload::before,#pullspin::before, + body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i,body.scan-any #scanAll .i{ + animation:ipxpulse .8s ease-in-out infinite alternate!important} + .toast,.card{animation:ipxfade .2s!important} } +@keyframes ipxpulse{to{opacity:.3}} +@keyframes ipxfade{from{opacity:0}} ::-webkit-scrollbar{width:10px;height:10px} ::-webkit-scrollbar-thumb{background:var(--raise);border-radius:6px} ::-webkit-scrollbar-track{background:transparent} @@ -523,7 +531,7 @@ a{color:var(--accent)} width:30px;height:30px;border-radius:8px;display:grid;place-items:center; color:var(--dim);flex:none; } -.iconbtn:hover{background:var(--raise);color:var(--fg)} +@media (hover:hover){.iconbtn:hover{background:var(--raise);color:var(--fg)}} /* One toolbar across the window, as the original had: grouped buttons, search on the right. */ /* The display's own intrusions -- the Dynamic Island and status bar along the top (#54), the home indicator along the bottom, the notch at one side in landscape. viewport-fit=cover hands the page the whole screen, which is what a standalone @@ -554,13 +562,13 @@ a{color:var(--accent)} .tgroup button,.tgroup a{padding:5px 11px;min-width:34px;color:var(--dim);font-size:14px;line-height:1.3} .tgroup a{display:inline-grid;place-items:center} .tgroup button+button,.tgroup button+a{border-left:1px solid var(--line)} -.tgroup button:hover:not(:disabled),.tgroup a:hover{background:var(--raise);color:var(--fg)} +@media (hover:hover){.tgroup button:hover:not(:disabled),.tgroup a:hover{background:var(--raise);color:var(--fg)}} .tgroup button:disabled{opacity:.35;cursor:default} .sidetools button,.sidefoot button{ flex:1;background:var(--panel2);border:1px solid var(--line);border-radius:8px; padding:6px 8px;font-size:12.5px;color:var(--dim); } -.sidetools button:hover,.sidefoot button:hover{border-color:var(--accent);color:var(--fg)} +@media (hover:hover){.sidetools button:hover,.sidefoot button:hover{border-color:var(--accent);color:var(--fg)}} /* Settings and the log are housekeeping: they sit under the feeds, out of the way. */ .sidefoot{ display:flex;flex-direction:column;gap:7px;padding:8px 12px; @@ -570,7 +578,7 @@ a{color:var(--accent)} .who{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--faint)} .who span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .who button{flex:none;background:none;border:0;padding:2px 4px;color:var(--faint);text-decoration:underline} -.who button:hover{color:var(--fg)} +@media (hover:hover){.who button:hover{color:var(--fg)}} .sidefoot button{display:inline-flex;align-items:center;justify-content:center;gap:6px} .sidefoot i{font-style:normal;opacity:.75} .searchwrap{padding:12px 12px 8px} @@ -588,18 +596,18 @@ input:focus,select:focus{border-color:var(--accent)} .clearable>input[type=search]{flex:1;min-width:0;padding-right:32px} .clearable>input::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none} .clearx{position:absolute;right:4px;top:50%;transform:translateY(-50%);display:grid;place-items:center;width:26px;height:26px;padding:0;border-radius:50%;color:var(--faint)} -.clearx:hover{color:var(--fg)} +@media (hover:hover){.clearx:hover{color:var(--fg)}} .clearx svg{width:11px;height:11px} .clearable:has(>input:placeholder-shown) .clearx{display:none} .inline .clearable{flex:1} /* The wider left gutter is the folder triangle's; everything in the list shifts with it, so the feeds still line up with the places above. */ -#feedlist{overflow-y:auto;padding:0 8px 12px 16px;flex:1;min-height:0} +#feedlist{overflow-y:auto;overscroll-behavior:contain;padding:0 8px 12px 16px;flex:1;min-height:0} .feed{ display:flex;gap:10px;align-items:center;padding:7px 8px;border-radius:9px; cursor:pointer;margin-bottom:1px;position:relative; } -.feed:hover{background:var(--panel2)} +@media (hover:hover){.feed:hover{background:var(--panel2)}} .feed.sel{background:var(--raise)} /* A show sits under its folder's title, a size down, so an open folder reads as one. */ .feed.child{margin-left:11px} @@ -628,7 +636,8 @@ input:focus,select:focus{border-color:var(--accent)} max-height:220px;overflow-y:auto;margin-top:8px; } .iconcell{height:40px;border-radius:8px;display:grid;place-items:center;color:var(--dim)} -.iconcell:hover,.iconcell[aria-selected="true"]{background:var(--raise);color:var(--fg)} +.iconcell[aria-selected="true"]{background:var(--raise);color:var(--fg)} +@media (hover:hover){.iconcell:hover{background:var(--raise);color:var(--fg)}} .iconcell .ficon{width:20px;height:20px} /* Auto in the icon picker, while an icon is picked: the scan spinner, beside the word. */ .dlink.busy::after{ @@ -642,7 +651,7 @@ input:focus,select:focus{border-color:var(--accent)} position:absolute;left:-16px;top:0;bottom:0;width:24px;display:grid;place-items:center; border-radius:4px;color:var(--faint); } -.chev:hover{color:var(--fg)} +@media (hover:hover){.chev:hover{color:var(--fg)}} .chev.bad,.chev.bad:hover{color:var(--bad)} /* A feed being checked: a small spinner in place of its unread count (issue #37). */ .feed.scanning .badge{display:none} @@ -672,7 +681,7 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, display:flex;gap:10px;align-items:center;padding:7px 9px;border:1px solid var(--line); border-radius:9px;cursor:pointer;background:var(--panel); } -.childrow:hover{border-color:var(--accent)} +@media (hover:hover){.childrow:hover{border-color:var(--accent)}} .childrow .art{width:32px;height:32px;font-size:12px} .childrow .txt{flex:1;min-width:0} .childrow .txt b{display:block;font-weight:500;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} @@ -700,7 +709,7 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, .dhead{margin-bottom:6px} .dback{display:inline-flex;align-items:center;gap:5px;margin:0 0 8px -5px;padding:3px 5px;border-radius:6px;font-size:13px;color:var(--accent)} .dback .i{width:11px;height:11px} -.dback:hover{background:var(--raise)} +@media (hover:hover){.dback:hover{background:var(--raise)}} .dtitle{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px 18px} .dtitle h2{margin:0;font-size:30px;line-height:1.15;font-weight:700;letter-spacing:-.02em;overflow-wrap:anywhere} .dhead .sub{margin:5px 0 0;color:var(--dim);font-size:13px;font-variant-numeric:tabular-nums} @@ -708,7 +717,7 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, .dsech{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:10px} .dsech h3{margin:0;font-size:19px;line-height:1.25;font-weight:650;letter-spacing:-.01em} .dall{font-size:13px;color:var(--accent);flex:none} -.dall:hover{text-decoration:underline} +@media (hover:hover){.dall:hover{text-decoration:underline}} /* The most subscribed, numbered because they are ranked: down one column, then the next. */ .chart{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:repeat(var(--rows,5),auto);grid-auto-flow:column;column-gap:32px} .rank{display:grid;grid-template-columns:22px 52px minmax(0,1fr) auto;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--line)} @@ -726,7 +735,7 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, display:flex;flex-direction:column;align-items:flex-start; background:var(--panel);border:1px solid var(--line); } -.cat:hover{border-color:var(--accent)} +@media (hover:hover){.cat:hover{border-color:var(--accent)}} /* The whole width: the fan sits below two lines of name, and a narrower name broke Government in two in the wider faces, Classic's and Glass's. */ .cat b{font-size:16px;line-height:1.2;font-weight:650;color:var(--fg);overflow-wrap:break-word} @@ -738,22 +747,22 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, to open, the browser drew the covers again on the first frames, and again on the way back, and the fan opened in a jerk (#132). All three move, for as long as the eye can follow, as one motion that settles rather than stops. */ - will-change:transform; + will-change:rotate,translate; transition:rotate .45s cubic-bezier(.2,.7,.2,1),translate .45s cubic-bezier(.2,.7,.2,1); } .cat .fan .art:nth-last-child(1){right:0;rotate:8deg;translate:0 -12px} .cat .fan .art:nth-last-child(2){right:25px;rotate:-3deg;translate:0 -4px} .cat .fan .art:nth-last-child(3){right:50px;rotate:-13deg;translate:0 4px} -.cat:hover .fan .art:nth-last-child(1){rotate:13deg;translate:5px -17px} -.cat:hover .fan .art:nth-last-child(2){rotate:-2deg;translate:0 -8px} -.cat:hover .fan .art:nth-last-child(3){rotate:-19deg;translate:-7px 3px} +@media (hover:hover){.cat:hover .fan .art:nth-last-child(1){rotate:13deg;translate:5px -17px}} +@media (hover:hover){.cat:hover .fan .art:nth-last-child(2){rotate:-2deg;translate:0 -8px}} +@media (hover:hover){.cat:hover .fan .art:nth-last-child(3){rotate:-19deg;translate:-7px 3px}} /* A category's subcategories, each with how many it holds, then the order. A picked one is drawn as a picked tab is. */ .dtools{display:flex;align-items:center;gap:10px 14px;margin-bottom:14px} .pills{display:flex;flex-wrap:wrap;gap:6px;flex:1 1 0;min-width:0} .pills button{flex:none;padding:4px 11px;border:1px solid var(--line);border-radius:999px;font-size:13px;color:var(--dim);white-space:nowrap} .pills button small{margin-left:6px;color:var(--faint);font-size:11.5px;font-variant-numeric:tabular-nums} -.pills button:hover{border-color:var(--accent);color:var(--fg)} +@media (hover:hover){.pills button:hover{border-color:var(--accent);color:var(--fg)}} .pills button.on{background:var(--raise);border-color:var(--raise);color:var(--fg);font-weight:500} .dtools .grow{flex:1} #dirsort{flex:none;width:auto;padding:4px 6px;font-size:13px} @@ -767,7 +776,7 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, .tile .txt small{display:block;color:var(--faint);font-size:11.5px} /* Every one opens: yours its feed, the rest their page here. */ .tile,.rank{cursor:pointer} -.tile:hover .txt b,.rank:hover .txt b{color:var(--accent)} +@media (hover:hover){.tile:hover .txt b,.rank:hover .txt b{color:var(--accent)}} /* A feed's page before you subscribe, as Apple's show page: the cover large, what it is and who here reads it beside it, then what it has published lately. */ .show{display:flex;gap:22px;align-items:flex-end;margin-top:4px} @@ -778,7 +787,7 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, .show .dfail{color:var(--bad)} .show .acts{margin-top:8px} .dlink{padding:0;font-size:13px;color:var(--accent);text-align:left} -.dlink:hover{text-decoration:underline} +@media (hover:hover){.dlink:hover{text-decoration:underline}} /* What the feed says it is, three lines of it until More. */ .about{max-width:760px;margin-top:18px} .about p{margin:0;color:var(--dim);font-size:14px;line-height:1.55;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;overflow-wrap:anywhere} @@ -788,7 +797,7 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, .lep{padding:12px 0;border-bottom:1px solid var(--line)} .lep b{display:block;font-size:15px;font-weight:600;line-height:1.35;overflow-wrap:anywhere} .lep b a{color:var(--fg);text-decoration:none} -.lep b a:hover{color:var(--accent)} +@media (hover:hover){.lep b a:hover{color:var(--accent)}} .lep p{margin:4px 0 0;color:var(--dim);font-size:13px;line-height:1.5;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden} .lep small{display:flex;align-items:center;gap:7px;margin-top:6px;color:var(--faint);font-size:12px;font-variant-numeric:tabular-nums} /* Outlined, not filled: on --raise the warning and error colours fell short of AA in most light @@ -802,6 +811,10 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, display:grid;place-items:center;color:var(--faint);font-weight:600;overflow:hidden; } .art>.tile{width:100%;height:100%;display:block} +/* A hairline inside the edge, so a dark cover keeps its boundary on a dark page (#313). Black or + white, not a palette colour, which shows as a tinted fringe; initials are not a picture. */ +.art:not(.ini){outline:1px solid oklch(1 0 0/.1);outline-offset:-1px} +:root[data-mode="light"] .art:not(.ini){outline-color:oklch(0 0 0/.1)} #pdead .tile{width:26px;height:26px;border-radius:6px} /* Initials, tinted by a hash of the name so two "TS" in a row can be told apart. Mixed into the page ground, not --raise: that is also the selected row, and a selected tile vanished into it. */ @@ -865,7 +878,7 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, #files .empty{padding:24px 0} .fhd{font-size:12px;color:var(--faint)} #grab{grid-area:grab;cursor:row-resize;background:var(--line)} -#grab:hover{background:var(--accent)} +@media (hover:hover){#grab:hover{background:var(--accent)}} #detail{grid-area:detail;overflow-y:auto;padding:16px 20px 28px;background:var(--panel);border-top:1px solid var(--line)} .dt{font-size:18px;font-weight:650;margin:0 0 5px;line-height:1.3;overflow-wrap:anywhere} .dmeta{color:var(--faint);font-size:12.5px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:14px} @@ -889,20 +902,20 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, .fhead h2{margin:0 0 3px;font-size:23px;line-height:1.2;overflow-wrap:anywhere} .fhead .sub{color:var(--dim);font-size:13px;margin-bottom:8px} .fhead .sub a{color:var(--dim);text-decoration:none} -.fhead .sub a:hover{color:var(--accent)} +@media (hover:hover){.fhead .sub a:hover{color:var(--accent)}} .acts{display:flex;gap:7px;align-items:center;margin-top:auto} .acts .btn{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:999px} .acts .btn i{font-style:normal;font-size:13px;line-height:1;opacity:.7} -.acts .btn:hover{background:var(--raise)} +@media (hover:hover){.acts .btn:hover{background:var(--raise)}} .btn{ background:var(--panel2);border:1px solid var(--line);border-radius:8px; padding:6px 12px;font-size:13px; } -.btn:hover{border-color:var(--accent)} +@media (hover:hover){.btn:hover{border-color:var(--accent)}} a.btn{text-decoration:none;color:inherit} .btn.primary{background:var(--accent);border-color:var(--accent);color:var(--ink);font-weight:600} -.btn.primary:hover{filter:brightness(1.08)} -.btn.danger:hover{border-color:var(--bad);color:var(--bad)} +@media (hover:hover){.btn.primary:hover{filter:brightness(1.08)}} +@media (hover:hover){.btn.danger:hover{border-color:var(--bad);color:var(--bad)}} /* An icon in place of a word; the word is in its tooltip. */ .btn.ico{padding:4px 9px;min-width:32px;font-size:14px;line-height:1.25;text-align:center} /* Inline in a sentence, next to a plain-word error explanation. */ @@ -927,7 +940,7 @@ kbd{font:inherit;font-size:12px;color:var(--fg);background:var(--panel2);border: display:flex;gap:10px;align-items:center;margin-bottom:12px;flex-wrap:wrap; position:sticky;top:0;background:var(--bg);padding:6px 0 8px;z-index:3; } -.tabs{display:flex;gap:2px;background:var(--panel2);border-radius:9px;padding:3px} +.tabs{display:flex;gap:2px;background:var(--panel2);border-radius:10px;padding:3px} .tabs button{padding:5px 12px;border-radius:7px;font-size:13px;color:var(--dim)} .tabs button.on{background:var(--raise);color:var(--fg);font-weight:500} .tabs a{padding:5px 12px;border-radius:7px;font-size:13px;color:var(--dim);text-decoration:none} @@ -967,7 +980,8 @@ body.adminpage #log{max-width:none} /* Each heading sorts by its column; the caret says which way. */ .ephead .hs{display:flex;align-items:center;gap:4px;min-width:0;padding:0;border:0;background:none; font:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;text-align:left;cursor:pointer} -.ephead .hs:hover,.ephead .hs.on{color:var(--fg)} +.ephead .hs.on{color:var(--fg)} +@media (hover:hover){.ephead .hs:hover{color:var(--fg)}} .ephead .hs .arr{display:inline-flex} .ephead .hs .arr .i{width:8px;height:8px} .ephead .hs .arr.asc{transform:rotate(-90deg)} @@ -979,7 +993,7 @@ body.adminpage #log{max-width:none} .ep{padding:4px 10px;border-radius:7px;border:1px solid transparent;cursor:pointer;margin-bottom:1px} .ep>*{min-width:0} .ep.sel{background:var(--raise);border-color:var(--line)} -.ep:hover{background:var(--panel)} +@media (hover:hover){.ep:hover{background:var(--panel)}} /* Amber means new: the unread dot, the badges and a download under way. Blue is for the one primary action and links, so a count no longer looks like a button. */ /* No padding: a button keeps the browser's side padding, which left too little room for the 16px @@ -1005,7 +1019,7 @@ body.playing .eq i{animation:eq .9s ease-in-out infinite alternate} body.playing .eq i:nth-child(1){animation-delay:-.3s} body.playing .eq i:nth-child(3){animation-delay:-.6s} @keyframes eq{from{transform:scaleY(.35)}} -.ep .st:hover,.ep .fl:hover{background:var(--raise)} +@media (hover:hover){.ep .st:hover,.ep .fl:hover{background:var(--raise)}} .ep .t{font-weight:600;font-size:13.5px;display:block;width:100%;padding:0;text-align:start;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} /* An item with no title shows its opening words: set as text, not as a heading. */ .ep .t.notitle{font-weight:400} @@ -1062,7 +1076,7 @@ body.has-video #audio{ width:40px!important;height:40px!important;background:var(--fg);color:var(--bg); border-radius:50%; } -#pplay:hover{background:var(--accent);color:var(--ink)} +@media (hover:hover){#pplay:hover{background:var(--accent);color:var(--ink)}} #seekrow{display:flex;gap:9px;align-items:center;font-variant-numeric:tabular-nums;font-size:11.5px;color:var(--faint)} input[type=range]{ -webkit-appearance:none;appearance:none;height:4px;border-radius:3px;flex:1; @@ -1087,6 +1101,7 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu .card{ background:var(--panel);border:1px solid var(--line);border-radius:14px; padding:20px;width:min(460px,100%);box-shadow:var(--shadow);max-height:88vh;overflow:auto; + overscroll-behavior:contain; } .card.wide{width:min(1000px,100%)} #logbox{ @@ -1124,7 +1139,7 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu /* A frame of its own: the body scrolls and Save stays at the foot, where on a phone it had been three screens down. */ .card.fset{padding:0;display:flex;flex-direction:column;overflow:hidden;width:min(580px,100%);container-type:inline-size} -.fsetbody{overflow:auto;min-height:0;padding:22px 24px 24px} +.fsetbody{overflow:auto;overscroll-behavior:contain;min-height:0;padding:22px 24px 24px} .card.fset .fsetfoot{flex:none;margin:0;padding:12px 24px;border-top:1px solid var(--line)} .fsethead{display:flex;gap:14px;align-items:center;margin-bottom:20px} .fsethead .art{width:56px;height:56px;border-radius:12px;font-size:18px} @@ -1219,7 +1234,7 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu .chips:focus-within{border-color:var(--accent)} .chip{display:inline-flex;align-items:center;gap:2px;max-width:100%;padding:3px 3px 3px 10px;border-radius:999px;background:var(--panel2);color:var(--fg);font-size:13px;line-height:1.3;overflow-wrap:anywhere} .chipx{display:grid;place-items:center;flex:none;width:20px;height:20px;padding:0;border-radius:50%;color:var(--dim)} -.chipx:hover{background:var(--raise);color:var(--fg)} +@media (hover:hover){.chipx:hover{background:var(--raise);color:var(--fg)}} .chipx .i{width:9px;height:9px} .chips .chipin{flex:1 0 8ch;width:auto;min-width:0;padding:4px 6px;border:0;background:none} #toasts{position:fixed;bottom:88px;right:18px;display:flex;flex-direction:column;gap:8px;z-index:60} @@ -1256,7 +1271,7 @@ body:has(#player.on) #status{padding-bottom:3px} /* Places above the feeds: not subscriptions, but where to look. */ .places{border-bottom:1px solid var(--line);margin:0 0 6px;padding-bottom:6px} .place{display:flex;gap:10px;align-items:center;padding:6px 8px;border-radius:9px;cursor:pointer} -.place:hover{background:var(--panel2)} +@media (hover:hover){.place:hover{background:var(--panel2)}} .place.sel{background:var(--raise)} .place .ico{width:18px;text-align:center;color:var(--accent);flex:none} .place b{flex:1;font-weight:500;font-size:13.5px} @@ -1374,10 +1389,8 @@ body:has(#player.on) #status{padding-bottom:3px} #player{border-top-color:var(--edge)} .sidefoot{border-top-color:var(--edge)} - /* Buttons: filled and round. Hover is reset because Safari keeps it on whatever was last - tapped, which left an accent outline on it. */ + /* Buttons: filled and round. */ .btn,.sidetools button,.sidefoot button,.tgroup,.toast,kbd,#logbox{border-color:var(--edge)} - .btn:hover,.sidetools button:hover,.sidefoot button:hover{border-color:var(--edge)} .btn,.sidetools button,.sidefoot button{border-radius:999px;min-height:40px;padding-left:14px;padding-right:14px} .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px} .btn.ico,#dback{min-width:40px;height:40px;padding:0 10px;display:inline-grid;place-items:center} @@ -1414,7 +1427,6 @@ body:has(#player.on) #status{padding-bottom:3px} /* Rows: taller, larger type, and a hairline between them that starts where the text does. */ .ep{padding:10px 8px;border-radius:10px;min-height:44px} .ep.sel{border-color:var(--edge)} - .ep:hover:not(.sel){background-color:transparent} .ep .t{font-size:17px} .ep .line,.ep .date{font-size:15px} .ep+.ep{background-image:linear-gradient(var(--line),var(--line));background-size:calc(100% - 64px) 1px;background-position:64px 0;background-repeat:no-repeat} @@ -1486,9 +1498,11 @@ body:has(#player.on) #status{padding-bottom:3px} :root[data-theme="classic"] .btn, :root[data-theme="classic"] .tabs{background:linear-gradient(#ffffff,#d9d9d9);border-color:#8e8e8e;color:#1a1a1a} :root[data-theme="classic"] .tabs{border:1px solid #8e8e8e;border-radius:6px} -:root[data-theme="classic"] .tgroup button:hover:not(:disabled), -:root[data-theme="classic"] .tgroup a:hover, -:root[data-theme="classic"] .btn:hover{background:linear-gradient(#f5f9ff,#cbdbf4)} +@media (hover:hover){ + :root[data-theme="classic"] .tgroup button:hover:not(:disabled), + :root[data-theme="classic"] .tgroup a:hover, + :root[data-theme="classic"] .btn:hover{background:linear-gradient(#f5f9ff,#cbdbf4)} +} :root[data-theme="classic"] .btn.primary, :root[data-theme="classic"] .tabs button.on, :root[data-theme="classic"] .feed.sel, @@ -1523,7 +1537,8 @@ body:has(#player.on) #status{padding-bottom:3px} :root[data-theme="classic"] .ep.sel .fl, :root[data-theme="classic"] .ep.sel .file, :root[data-theme="classic"] .ep.sel .kind, -:root[data-theme="classic"] .ep.sel .iconbtn:not(:hover){color:#fff} +:root[data-theme="classic"] .ep.sel .iconbtn{color:#fff} +@media (hover:hover){:root[data-theme="classic"] .ep.sel .iconbtn:hover{color:var(--fg)}} /* Green and red stay green and red on the blue, just lighter so they read. */ :root[data-theme="classic"] .ep.sel .kind.here{color:#a6f3a6} :root[data-theme="classic"] .ep.sel .kind.bad{color:#ffb8ad} diff --git a/web/src/feeds.ts b/web/src/feeds.ts index d86a585..8881e52 100644 --- a/web/src/feeds.ts +++ b/web/src/feeds.ts @@ -60,7 +60,16 @@ function paintScanning(){ if(on) row.title='Checking for new items…'; else row.removeAttribute('title'); } // A feed's button turns on its own page only, wherever the person has gone since pressing it. - document.body.classList.toggle('scan-this',spinFeed!==null&&spinFeed===S.feed); + const self=spinFeed!==null&&spinFeed===S.feed, any=document.body.classList.contains('scan-any'); + document.body.classList.toggle('scan-this',self); + // Turning was all that said a check was running, and with motion reduced nothing turns (#310). + for(const b of $$('#scanAll,.fhead [data-a=scanall],.fhead [data-a=scan]')){ + const on=b.dataset.a==='scan'?self:any; + b.dataset.label??=b.getAttribute('aria-label')!; + if(on) b.setAttribute('aria-busy','true'); else b.removeAttribute('aria-busy'); + b.title=on?'Checking for new items…':b.dataset.label; + b.setAttribute('aria-label',b.title); + } } /// The refresh buttons turn from the press until that scan's scan_done (#78, #233). On , /// because a feed's page is redrawn as its items come in and would take a class on the button @@ -68,15 +77,16 @@ function paintScanning(){ /// between one feed and the next, many times a second (#231). let spinFeed: string|null=null; function spinStart(feed: string|null){ - if(feed===null) document.body.classList.add('scan-any'); - else { spinFeed=feed; paintScanning(); } + if(feed===null) document.body.classList.add('scan-any'); else spinFeed=feed; + paintScanning(); } /// `only` and `forced` from scan_done: a scheduled scan, or one limited to another feed, leaves /// a pressed button turning, since the scan it asked for is still queued behind it. Nothing /// (an error ended the job) stops them all. function spinEnd(done?: {only: string|null, forced: boolean}){ if(!done||done.forced&&done.only==null) document.body.classList.remove('scan-any'); - if(!done||done.forced&&done.only===spinFeed){ spinFeed=null; paintScanning(); } + if(!done||done.forced&&done.only===spinFeed) spinFeed=null; + paintScanning(); } function renderFeeds(){