Fix the accessibility findings from the audit (#12)
Contrast: --mute-2 and --trash both failed AA in both themes — every 10px mono label (recent chapter, section headings, chapter hint) sat between 2.5:1 and 3.7:1, and the trash icon, the one irreversible control, was the least visible thing in the strip. Retuned per theme: labels now 4.8-5.2:1, trash 4.3-4.5:1 against a 3:1 requirement. Focus: the search input clears its own outline and the underline lives on the parent, so :focus-within on .searchbar carries the ring. The inline error slot gets role=status, so a failed write is announced rather than looking like an ignored tap. The tab strip drops role=tablist/tab: these are real links that change the URL, and the roles promised a tabpanel contract that was never implemented, leaving aria-selected unset on every one. aria-current carries the state instead. prefers-reduced-motion now names pseudo-elements — `*` does not match them, so the busy bar and the error dot, both ::before, kept animating for the users who asked them not to. Log out was a 54x15 target; an ::after pad makes it 78x45 without moving the label. Also: the two dark-tuned hex borders in the desktop block become theme-aware tokens, and the preload swaps mono for DM Sans, the larger face and the one the body text needs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,11 @@
|
||||
})();
|
||||
|
||||
function setActiveTab(el) {
|
||||
el.parentElement.querySelectorAll("[role=tab]").forEach(function (t) {
|
||||
t.classList.toggle("active", t === el);
|
||||
el.parentElement.querySelectorAll("a").forEach(function (t) {
|
||||
var on = t === el;
|
||||
t.classList.toggle("active", on);
|
||||
if (on) t.setAttribute("aria-current", "page");
|
||||
else t.removeAttribute("aria-current");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+29
-11
@@ -64,7 +64,9 @@
|
||||
--paper-hot: #f0d3cb; /* title of a series with a new chapter */
|
||||
--paper-dim: #ddd5cb; /* resting title */
|
||||
--mute: #8d857c; /* secondary text, idle icons */
|
||||
--mute-2: #5a5450; /* labels, hints */
|
||||
/* Every mute-2 use is 10px mono, so it has to clear 4.5:1 on --ink even
|
||||
though it reads as the quietest step. */
|
||||
--mute-2: #877f76; /* labels, hints */
|
||||
--faint: #3a3733; /* meta separators */
|
||||
--faint-2: #57504b; /* cover monogram */
|
||||
|
||||
@@ -73,7 +75,11 @@
|
||||
--ember-ink: #150907; /* text on solid ember */
|
||||
--ember-soft: #eda798; /* text on ember wash */
|
||||
--brass: #b8912f; /* favourite — a second, cooler metal */
|
||||
--trash: #6b5450; /* remove, resting */
|
||||
--trash: #977671; /* remove, resting — icons need 3:1, not 4.5:1 */
|
||||
|
||||
/* Desktop cell borders for the two coloured action states. */
|
||||
--play-hot-line: #3a1d18;
|
||||
--fav-line: #332b14;
|
||||
|
||||
--asura: #7d93a5;
|
||||
--demonic: #a98a78;
|
||||
@@ -106,7 +112,7 @@
|
||||
--paper-hot: #a33018;
|
||||
--paper-dim: #191715;
|
||||
--mute: #6b645d;
|
||||
--mute-2: #857d75;
|
||||
--mute-2: #6c655e; /* clears 4.5:1 on --ash too, not just --ink */
|
||||
--faint: #c9c2ba;
|
||||
--faint-2: #a8a098;
|
||||
|
||||
@@ -115,7 +121,10 @@
|
||||
--ember-ink: #fff;
|
||||
--ember-soft: #8d2c17;
|
||||
--brass: #8a681c;
|
||||
--trash: #a98276;
|
||||
--trash: #8c6558;
|
||||
|
||||
--play-hot-line: #f0cfc6;
|
||||
--fav-line: #e3d3a4;
|
||||
|
||||
--asura: #4f6b80;
|
||||
--demonic: #8a6a55;
|
||||
@@ -180,6 +189,7 @@ button { cursor: pointer; }
|
||||
.topbar form { margin: 0; }
|
||||
|
||||
.ghost {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--field-line);
|
||||
@@ -190,6 +200,8 @@ button { cursor: pointer; }
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.ghost:hover { color: var(--paper); border-bottom-color: var(--paper); }
|
||||
/* The label is 15px tall by design; the thumb gets 44 without moving it. */
|
||||
.ghost::after { content: ""; position: absolute; inset: -15px -12px; }
|
||||
|
||||
.chrome { display: flex; flex-direction: column; }
|
||||
|
||||
@@ -202,6 +214,9 @@ button { cursor: pointer; }
|
||||
border-bottom: 1px solid var(--rule);
|
||||
color: var(--mute-2);
|
||||
}
|
||||
/* The input drops its own outline, so the bar it sits in carries the focus
|
||||
ring — same move the two other inputs make with their border. */
|
||||
.searchbar:focus-within { border-bottom-color: var(--paper); color: var(--paper); }
|
||||
.searchbar svg { width: 15px; height: 15px; flex: none; }
|
||||
.search {
|
||||
flex: 1;
|
||||
@@ -226,7 +241,7 @@ button { cursor: pointer; }
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.tabs::-webkit-scrollbar { display: none; }
|
||||
.tabs [role="tab"] {
|
||||
.tabs a {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -236,8 +251,8 @@ button { cursor: pointer; }
|
||||
font: 400 17px var(--font-display);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tabs [role="tab"]:hover { color: var(--paper-dim); }
|
||||
.tabs [role="tab"].active {
|
||||
.tabs a:hover { color: var(--paper-dim); }
|
||||
.tabs a.active {
|
||||
color: var(--paper);
|
||||
border-bottom: 2px solid var(--paper);
|
||||
margin-bottom: -1px;
|
||||
@@ -661,7 +676,7 @@ button { cursor: pointer; }
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.tabs { order: 1; flex: none; gap: 20px; padding: 0; border-bottom: none; }
|
||||
.tabs [role="tab"] { padding: 10px 0 14px; font-size: 18px; }
|
||||
.tabs a { padding: 10px 0 14px; font-size: 18px; }
|
||||
.searchbar { order: 2; flex: 1; margin: 0; border-bottom: none; }
|
||||
.recent h2, .recent-strip { padding-left: 32px; padding-right: 32px; }
|
||||
.recent-card, .recent-cover { width: 100px; }
|
||||
@@ -683,8 +698,8 @@ button { cursor: pointer; }
|
||||
than by the hairline the phone layout uses. */
|
||||
.actions > .play + *,
|
||||
.actions > *:not(.lifecycle) + .lifecycle { margin-left: 10px; box-shadow: none; }
|
||||
.is-new .actions .play { border-color: #3a1d18; }
|
||||
.actions .on { border-color: #332b14; }
|
||||
.is-new .actions .play { border-color: var(--play-hot-line); }
|
||||
.actions .on { border-color: var(--fav-line); }
|
||||
|
||||
/* Panels line up with the body text, i.e. past the cover and its gap. */
|
||||
.chapter-form, .confirm-row, .error-inline {
|
||||
@@ -693,5 +708,8 @@ button { cursor: pointer; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { animation: none !important; transition: none !important; }
|
||||
/* Pseudo-elements need naming explicitly — `*` does not match them, and the
|
||||
busy bar and error dot are both ::before. Their static form still reads:
|
||||
the bar stays drawn and .actions stays dimmed. */
|
||||
*, *::before, *::after { animation: none !important; transition: none !important; }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<title>mangaBookmark</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="preload" href="/static/fonts/instrument-serif-400-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/static/fonts/ibm-plex-mono-500-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
{{/* Body text before meta lines: DM Sans is the biggest face and the one
|
||||
most of the page is set in; the mono is small and arrives from CSS. */}}
|
||||
<link rel="preload" href="/static/fonts/dm-sans-var-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<script src="/static/htmx.min.js" defer></script>
|
||||
<script src="/static/filter.js" defer></script>
|
||||
</head>
|
||||
@@ -31,21 +33,29 @@
|
||||
autocomplete="off" aria-label="Search titles">
|
||||
</div>
|
||||
|
||||
<nav class="tabs" role="tablist">
|
||||
<a role="tab" href="/?tab=all" class="{{if eq .Tab "all"}}active{{end}}"
|
||||
{{/* These are real links with real hrefs that change the URL, so they are
|
||||
navigation, not an ARIA tablist — aria-current carries "which bucket am
|
||||
I in" without owing a tabpanel contract we do not implement. */}}
|
||||
<nav class="tabs" aria-label="Bookmark buckets">
|
||||
<a href="/?tab=all" class="{{if eq .Tab "all"}}active{{end}}"
|
||||
{{if eq .Tab "all"}}aria-current="page"{{end}}
|
||||
hx-get="/ui/list?tab=all" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=all" hx-on::after-request="setActiveTab(this)">All</a>
|
||||
<a role="tab" href="/?tab=new" class="tab-new {{if eq .Tab "new"}}active{{end}}"
|
||||
<a href="/?tab=new" class="tab-new {{if eq .Tab "new"}}active{{end}}"
|
||||
{{if eq .Tab "new"}}aria-current="page"{{end}}
|
||||
hx-get="/ui/list?tab=new" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=new" hx-on::after-request="setActiveTab(this)">Updated{{if .NewCount}}
|
||||
<span class="count">{{.NewCount}}</span>{{end}}</a>
|
||||
<a role="tab" href="/?tab=fav" class="{{if eq .Tab "fav"}}active{{end}}"
|
||||
<a href="/?tab=fav" class="{{if eq .Tab "fav"}}active{{end}}"
|
||||
{{if eq .Tab "fav"}}aria-current="page"{{end}}
|
||||
hx-get="/ui/list?tab=fav" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=fav" hx-on::after-request="setActiveTab(this)">Favourites</a>
|
||||
<a role="tab" href="/?tab=archived" class="{{if eq .Tab "archived"}}active{{end}}"
|
||||
<a href="/?tab=archived" class="{{if eq .Tab "archived"}}active{{end}}"
|
||||
{{if eq .Tab "archived"}}aria-current="page"{{end}}
|
||||
hx-get="/ui/list?tab=archived" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=archived" hx-on::after-request="setActiveTab(this)">Archived</a>
|
||||
<a role="tab" href="/?tab=finished" class="{{if eq .Tab "finished"}}active{{end}}"
|
||||
<a href="/?tab=finished" class="{{if eq .Tab "finished"}}active{{end}}"
|
||||
{{if eq .Tab "finished"}}aria-current="page"{{end}}
|
||||
hx-get="/ui/list?tab=finished" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=finished" hx-on::after-request="setActiveTab(this)">Finished</a>
|
||||
</nav>
|
||||
|
||||
@@ -131,6 +131,8 @@
|
||||
<button type="button" onclick="toggleConfirmRow('{{.Key}}', 'remove')">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="error-inline" hidden></p>
|
||||
{{/* role=status announces a failed write; without it the tap just looks
|
||||
ignored to a screen reader. */}}
|
||||
<p class="error-inline" role="status" hidden></p>
|
||||
</article>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user