Keyboard, screen reader and contrast fixes from the interface review (#298 #299 #300 #301 #302 #303 #304 #305 #306 #307 #308 #309)

Found reviewing the main window, its dialogs and the Directory in all 25 palettes (13 themes,
light and dark), in Playwright against the test daemon.

- #298: an item's title is a button, so Tab and a screen reader can open it; the feed list's
  rows, places, Signals and the Directory's shows are links to a screen reader (asLink), named
  by what they show, the open one aria-current. Not role=button, which would flatten a folder's
  triangle inside it.
- #299: .ep:focus-within shows the row's actions; Tab reached Play and Delete at opacity 0.
- #300: the read toggle is named Mark read / Mark unread, the dot hidden from screen readers.
- #301: the seek slider is named Position, its value read as "12:30 of 45:00".
- #302: ::placeholder in --faint; the browser's grey was 2.7-4.4:1 in 20 palettes. The search
  and filter boxes have names that do not depend on their placeholder.
- #303: #toasts is role=status; an error toast stays until dismissed, at most four at once.
- #304: api() says what to do when iPX cannot be reached or answers 500 (whose text is the
  daemon's internals, already in its log); play() failures are put in words and reported, an
  AbortError ignored; a failed download names its item.
- #305, #308: --field, the theme's --fg mixed into --bg in OKLab (--fieldmix, 60% and up to 80%
  for Catppuccin light), edges text boxes, selects, chips, the OPML box and switch tracks,
  phones included, and rings a light theme's switch knob. tests/contrast.js holds it to 3:1 on
  bg, panel, panel2 and raise in every palette. --line stays for rules. Fields lose outline:0,
  so they take the same focus ring as everything else.
- #306: openModal makes the card role=dialog aria-modal, labelled by its heading, focuses it,
  and makes the rest of the body inert until closeModal, which gives focus back to what opened
  it. Not <dialog>: its top layer would cover the toasts and their Dismiss.
- #307: the labels in Add a feed, Settings, Download latest and OPML point at their boxes.
- #309: Settings shows iPX's and the page's versions at its foot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 20:12:34 +00:00
parent c8afcec73d
commit 6ec2c35291
16 changed files with 218 additions and 47 deletions

View File

@@ -20,6 +20,12 @@
--warn:#f5a524; /* amber, apart from the needle's orange, for pending */
--bad:#ff6b7a; /* pushed towards pink, apart from the needle */
--shadow:0 8px 28px rgba(4,10,20,.55);
/* The edge of a text box or a switch: the theme's own text mixed into its ground, as far as it
takes to clear 3:1 on every surface one sits on (#305, #308). --line is for rules between
things, which need not, and a box drawn in it all but vanished. A theme whose grounds sit
further apart takes more; tests/contrast.js holds every palette to it. */
--fieldmix:60%;
--field:color-mix(in oklab,var(--fg) var(--fieldmix),var(--bg));
}
/* Every other theme overrides the same variables, under data-theme and data-mode. The page's
script sets data-mode to light or dark, working Auto out from the system, so each theme has
@@ -132,6 +138,7 @@
--shadow:0 8px 28px rgba(0,0,0,.5);
}
:root[data-theme="adwaita"][data-mode="light"] {
--fieldmix:65%;
--bg:#ffffff;
--panel:#ebebed;
--panel2:#fafafb;
@@ -206,6 +213,7 @@
/* Nordic: the Nord palette. Polar Night for the dark half, Snow Storm for the light, Frost for
the accent and Aurora for the rest. Aurora red and Frost blue are shifted until they read. */
:root[data-theme="nordic"] {
--fieldmix:65%;
--bg:#2e3440; /* nord0 */
--panel:#3b4252; /* nord1 */
--panel2:#434c5e; /* nord2 */
@@ -243,6 +251,7 @@
mantle and the surfaces for the grounds, text and subtext for type, mauve (its default accent)
and peach for the rest. */
:root[data-theme="catppuccin"] {
--fieldmix:70%;
--bg:#1e1e2e; /* base */
--panel:#181825; /* mantle */
--panel2:#313244; /* surface0 */
@@ -260,6 +269,7 @@
--shadow:0 8px 28px rgba(0,0,0,.45);
}
:root[data-theme="catppuccin"][data-mode="light"] {
--fieldmix:80%;
--bg:#eff1f5; /* base */
--panel:#e6e9ef; /* mantle */
--panel2:#dce0e8; /* crust */
@@ -296,6 +306,7 @@
--shadow:0 8px 28px rgba(0,0,0,.45);
}
:root[data-theme="gruvbox"][data-mode="light"] {
--fieldmix:70%;
--bg:#fbf1c7; /* bg0 */
--panel:#f2e5bc; /* bg0_s */
--panel2:#ebdbb2; /* bg1 */
@@ -332,6 +343,7 @@
--shadow:0 8px 28px rgba(0,0,0,.45);
}
:root[data-theme="solarized"][data-mode="light"] {
--fieldmix:65%;
--bg:#fdf6e3; /* base3 */
--panel:#eee8d5; /* base2 */
--panel2:#e6dfca;
@@ -562,11 +574,13 @@ a{color:var(--accent)}
.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}
/* The browser's own grey fell short of AA in most themes, and a placeholder is all some boxes say (#302). */
::placeholder{color:var(--faint);opacity:1}
input[type=search],input[type=text],input[type=password],input[type=number],select{
width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg);
width:100%;background:var(--bg);border:1px solid var(--field);color:var(--fg);
border-radius:8px;padding:7px 10px;font:inherit;font-size:13.5px;
}
input:focus,select:focus{outline:0;border-color:var(--accent)}
input:focus,select:focus{border-color:var(--accent)}
/* A search box and the x that clears it, inside its right end, there only once something is typed
(#291). The browser's own x, in some browsers and not others, gives way to it. */
.clearable{position:relative;display:flex;align-items:center;min-width:0}
@@ -992,7 +1006,7 @@ 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)}
.ep .t{font-weight:600;font-size:13.5px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.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}
.ep.read .t{color:var(--dim);font-weight:500}
@@ -1009,7 +1023,8 @@ body.playing .eq i:nth-child(3){animation-delay:-.6s}
#split.one .h-fd,#split.one .ep .fd{display:none}
.dot{width:3px;height:3px;border-radius:50%;background:var(--faint);flex:none}
.ep .rowacts{display:flex;gap:2px;align-items:flex-start;opacity:0;transition:opacity .12s}
.ep:hover .rowacts{opacity:1}
/* Focus shows them as hover does: Tab reached Play and Delete while they were still invisible (#299). */
.ep:hover .rowacts,.ep:focus-within .rowacts{opacity:1}
.dlbar{height:3px;background:transparent;border-radius:2px;overflow:hidden;margin-top:7px}
.dlbar.live{background:var(--raise)}
.dlbar i{display:block;height:100%;width:0;background:var(--accent2);transition:width .25s}
@@ -1067,6 +1082,8 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu
place-items:center;z-index:50;padding:20px;
}
#modal.on{display:grid}
/* Focus lands on the card itself as a dialog opens (#306): a place to start from, not a control. */
#modalCard:focus{outline:none}
.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;
@@ -1138,10 +1155,12 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu
.switch.lead{font-weight:600}
.sw{position:relative;flex:none;width:42px;height:26px}
.sw input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
.sw i{position:absolute;inset:0;border-radius:999px;background:var(--raise);border:1px solid var(--line);pointer-events:none;transition:background-color .18s,border-color .18s}
.sw i{position:absolute;inset:0;border-radius:999px;background:var(--raise);border:1px solid var(--field);pointer-events:none;transition:background-color .18s,border-color .18s}
/* White, as iOS has it, in every theme: in --ink, the dark themes' knob was a black hole in the track. */
.sw i::after{content:"";position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.35);transition:transform .18s}
.sw input:checked+i{background:var(--accent);border-color:var(--accent)}
/* White on a light track did not show where the knob was, or so whether it was on (#308). */
:root[data-mode="light"] .sw i::after{box-shadow:0 0 0 1px var(--field),0 1px 3px rgba(0,0,0,.35)}
.sw input:checked+i::after{transform:translateX(16px)}
.sw input:focus-visible+i{outline:2px solid var(--accent);outline-offset:2px}
/* What only matters while it downloads on its own folds away when it is off: the rows in a grid of
@@ -1196,7 +1215,7 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu
.flabel{font-size:14px;font-weight:500}
.cap{margin:0;font-size:12.5px;line-height:1.45;color:var(--dim)}
/* Words and phrases as chips: in one comma-separated box, one ran into the next. */
.chips{display:flex;flex-wrap:wrap;gap:6px;align-items:center;min-height:38px;padding:5px;border:1px solid var(--line);border-radius:9px;background:var(--bg);cursor:text}
.chips{display:flex;flex-wrap:wrap;gap:6px;align-items:center;min-height:38px;padding:5px;border:1px solid var(--field);border-radius:9px;background:var(--bg);cursor:text}
.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)}
@@ -1209,7 +1228,9 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu
padding:9px 13px;font-size:13px;box-shadow:var(--shadow);animation:in .18s;max-width:340px;
}
/* On --panel, not a toast's --raise: the error colours are tuned to read on the page's grounds. */
.toast.bad{border-color:var(--bad);color:var(--bad);background:var(--panel)}
.toast.bad{border-color:var(--bad);color:var(--bad);background:var(--panel);display:flex;gap:10px;align-items:flex-start}
.toastx{flex:none;display:grid;place-items:center;width:24px;height:24px;margin:-3px -7px -3px 0;border-radius:6px;color:inherit}
.toastx svg{width:11px;height:11px}
@keyframes in{from{opacity:0;transform:translateY(6px)}}
#burger,#dback{display:none}
/* Totals for what is showing, along the bottom, as the original's status bar. */
@@ -1368,7 +1389,7 @@ body:has(#player.on) #status{padding-bottom:3px}
/* Text boxes filled, and at 16px or more: Safari zooms the page in on one any smaller. */
input[type=search],input[type=text],input[type=password],input[type=number],select{
font-size:17px;background:var(--panel2);border-color:var(--edge);border-radius:10px;padding:9px 12px;
font-size:17px;background:var(--panel2);border-color:var(--field);border-radius:10px;padding:9px 12px;
}
#topbar #epSearch{font-size:16px;border-radius:999px;padding:9px 34px 9px 14px}
input:focus,select:focus{border-color:var(--accent)}
@@ -1436,7 +1457,7 @@ body:has(#player.on) #status{padding-bottom:3px}
.dpends{font-size:14px}
/* 16px or more, or Safari zooms the page in on it. */
.wcap input[type=number]{font-size:16px;padding:3px 4px}
.chips{background:var(--panel2);border-color:var(--edge);border-radius:10px}
.chips{background:var(--panel2);border-color:var(--field);border-radius:10px}
.chip{background:var(--panel);font-size:15px;padding:5px 4px 5px 12px}
.chipx{width:26px;height:26px}
.field.boxed{border-radius:14px}