Fixes from the UI-polish review of the main window (2026-10-19). Reduced motion took every animation away, and a turning icon was the only sign a check was running. The refresh buttons now carry aria-busy and a "Checking for new items" label while checking, and under reduced motion spinners pulse and toasts and sheets fade. Every :hover rule is behind @media (hover:hover), since Safari keeps :hover on the last thing tapped; the two phone resets that undid it for .btn and .ep had nothing left to do and are gone. An item row's actions still show on hover without the query, so a tablet with no pointer can reach them by tapping the row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 } });
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
95
web/app.css
95
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}
|
||||
@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}
|
||||
|
||||
@@ -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 <body>,
|
||||
/// 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(){
|
||||
|
||||
Reference in New Issue
Block a user