Refactor chapter page: semantic HTML, accessibility improvements, Tailwind hover classes, and book page summary zoom overlay

This commit is contained in:
Admin
2026-03-01 23:03:18 +05:00
parent 5f4b1d96f6
commit e4624d0a0c

View File

@@ -1802,13 +1802,15 @@ func (s *Server) handleBookChaptersPage(w http.ResponseWriter, r *http.Request)
// ─── GET /books/{slug}/chapters/{n} — chapter reader ─────────────────────────
const chapterTmpl = `
<!-- ─── Sticky reader toolbar (nav, not header, so Reader Mode ignores it) ─── -->
<!-- aria-hidden keeps it out of accessibility trees / Reader Mode heuristics -->
<nav id="reader-header" aria-hidden="true"
style="position:sticky;top:0;z-index:50;background:#09090b;border-bottom:1px solid #27272a;">
<!-- ─── Sticky reader toolbar ────────────────────────────────────────────────
Uses <nav> with a descriptive aria-label so screen readers expose it as a
landmark. aria-hidden was previously misused here — removed. -->
<nav id="reader-header" aria-label="Reader controls"
class="sticky top-0 z-50 bg-zinc-950 border-b border-zinc-800">
<div class="max-w-2xl mx-auto px-4 flex items-center gap-2 h-14">
<!-- Back to book (book/list icon, visually distinct from ← Prev) -->
<!-- Back to book list -->
<a href="/books/{{.Slug}}"
hx-get="/books/{{.Slug}}"
hx-target="#main-content"
@@ -1816,8 +1818,7 @@ const chapterTmpl = `
hx-swap="innerHTML"
title="Back to chapter list"
aria-label="Back to chapter list"
style="flex-shrink:0;padding:0.375rem 0.5rem;border-radius:0.5rem;color:#a1a1aa;font-size:1rem;text-decoration:none;transition:color 0.15s;line-height:1;"
onmouseover="this.style.color='#fbbf24'" onmouseout="this.style.color='#a1a1aa'">
class="flex-shrink-0 px-2 py-1.5 rounded-lg text-zinc-400 hover:text-amber-400 transition-colors text-base leading-none no-underline">
&#9776;
</a>
@@ -1830,20 +1831,21 @@ const chapterTmpl = `
hx-swap="innerHTML"
title="Previous chapter"
aria-label="Previous chapter"
style="flex-shrink:0;padding:0.375rem 0.625rem;border-radius:0.5rem;background:transparent;color:#a1a1aa;font-size:0.8125rem;text-decoration:none;transition:color 0.15s;"
onmouseover="this.style.color='#f4f4f5'" onmouseout="this.style.color='#a1a1aa'">
class="flex-shrink-0 px-2.5 py-1.5 rounded-lg text-zinc-400 hover:text-zinc-100 text-[0.8125rem] transition-colors no-underline">
&#8592; Prev
</a>
{{end}}
<!-- Chapter title button — opens chapter list drawer -->
<button id="chapter-list-btn"
type="button"
onclick="toggleChapterList()"
aria-haspopup="listbox"
aria-expanded="false"
title="Jump to chapter"
aria-label="Jump to chapter"
style="flex:1;min-width:0;display:flex;align-items:center;justify-content:center;gap:0.375rem;overflow:hidden;background:transparent;border:none;cursor:pointer;padding:0.25rem 0.25rem;">
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.8125rem;color:#a1a1aa;max-width:100%;">{{.Title}}</span>
<span style="flex-shrink:0;font-size:0.625rem;color:#52525b;" aria-hidden="true">&#9660;</span>
class="flex-1 min-w-0 flex items-center justify-center gap-1.5 overflow-hidden bg-transparent border-none cursor-pointer py-1 px-1">
<span class="overflow-hidden text-ellipsis whitespace-nowrap text-[0.8125rem] text-zinc-400 max-w-full">{{.Title}}</span>
<span class="flex-shrink-0 text-[0.625rem] text-zinc-600" aria-hidden="true">&#9660;</span>
</button>
<!-- Next chapter -->
@@ -1855,97 +1857,101 @@ const chapterTmpl = `
hx-swap="innerHTML"
title="Next chapter"
aria-label="Next chapter"
style="flex-shrink:0;padding:0.375rem 0.625rem;border-radius:0.5rem;background:transparent;color:#a1a1aa;font-size:0.8125rem;text-decoration:none;transition:color 0.15s;"
onmouseover="this.style.color='#f4f4f5'" onmouseout="this.style.color='#a1a1aa'">
class="flex-shrink-0 px-2.5 py-1.5 rounded-lg text-zinc-400 hover:text-zinc-100 text-[0.8125rem] transition-colors no-underline">
Next &#8594;
</a>
{{end}}
<!-- TTS play/pause button -->
<!-- TTS play/pause -->
<button id="tts-btn"
type="button"
onclick="ttsToggle()"
title="Listen"
aria-label="Listen"
style="flex-shrink:0;display:flex;align-items:center;gap:0.375rem;padding:0.375rem 0.625rem;border-radius:0.5rem;background:transparent;color:#f59e0b;font-size:0.8125rem;font-weight:500;border:none;cursor:pointer;transition:color 0.15s;"
onmouseover="this.style.color='#fbbf24'" onmouseout="this.style.color='#f59e0b'">
class="flex-shrink-0 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-transparent text-amber-500 hover:text-amber-400 text-[0.8125rem] font-medium border-none cursor-pointer transition-colors">
<span id="tts-icon" aria-hidden="true">&#9654;</span>
<span id="tts-label">Listen</span>
</button>
<!-- Settings gear button -->
<!-- Settings -->
<button id="settings-btn"
type="button"
onclick="toggleSettings()"
title="Settings"
aria-label="Settings"
style="flex-shrink:0;padding:0.375rem 0.5rem;border-radius:0.5rem;background:transparent;color:#a1a1aa;font-size:1rem;border:none;cursor:pointer;transition:color 0.15s;"
onmouseover="this.style.color='#fbbf24'" onmouseout="this.style.color='#a1a1aa'">
aria-label="Reader settings"
aria-haspopup="dialog"
class="flex-shrink-0 px-2 py-1.5 rounded-lg bg-transparent text-zinc-400 hover:text-amber-400 text-base border-none cursor-pointer transition-colors">
&#9881;
</button>
</div>
<!-- TTS status strip -->
<div id="tts-status-bar" style="display:none;padding:0.125rem 1rem;background:#18181b;font-size:0.75rem;color:#71717a;text-align:center;" aria-live="polite">
<div id="tts-status-bar" hidden
class="px-4 py-0.5 bg-zinc-900 text-xs text-zinc-500 text-center"
aria-live="polite" aria-atomic="true">
<span id="tts-status"></span>
</div>
<!-- Chapter list drawer -->
<div id="chapter-list-panel"
style="display:none;position:absolute;left:0;right:0;top:100%;max-height:60vh;overflow-y:auto;background:#18181b;border-bottom:1px solid #27272a;box-shadow:0 8px 24px rgba(0,0,0,0.6);z-index:99;">
<div style="max-width:42rem;margin:0 auto;padding:0.5rem 0;">
role="listbox"
aria-label="Chapter list"
hidden
class="absolute left-0 right-0 top-full max-h-[60vh] overflow-y-auto bg-zinc-900 border-b border-zinc-800 shadow-2xl z-[99]">
<div class="max-w-2xl mx-auto py-2">
{{range .AllChapters}}
<a href="/books/{{$.Slug}}/chapters/{{.Number}}"
hx-get="/books/{{$.Slug}}/chapters/{{.Number}}"
hx-target="#main-content"
hx-push-url="true"
hx-swap="innerHTML"
role="option"
aria-selected="{{if eq .Number $.ChapterN}}true{{else}}false{{end}}"
onclick="closeChapterList()"
{{if eq .Number $.ChapterN}}data-current="1"{{end}}
style="display:flex;align-items:baseline;gap:0.75rem;padding:0.5rem 1rem;text-decoration:none;transition:background 0.1s;{{if eq .Number $.ChapterN}}background:#27272a;{{end}}"
onmouseover="this.style.background='#27272a'" onmouseout="this.style.background='{{if eq .Number $.ChapterN}}#27272a{{else}}transparent{{end}}'">
<span style="flex-shrink:0;font-size:0.75rem;color:#52525b;width:2.5rem;text-align:right;" aria-hidden="true">{{.Number}}</span>
<span style="font-size:0.875rem;{{if eq .Number $.ChapterN}}color:#fbbf24;font-weight:500;{{else}}color:#d4d4d8;{{end}}">{{if .Title}}{{.Title}}{{else}}Chapter {{.Number}}{{end}}</span>
{{if eq .Number $.ChapterN}}<span style="flex-shrink:0;font-size:0.7rem;color:#f59e0b;margin-left:auto;" aria-hidden="true">&#9654; now</span>{{end}}
class="flex items-baseline gap-3 px-4 py-2 no-underline transition-colors hover:bg-zinc-800 {{if eq .Number $.ChapterN}}bg-zinc-800{{end}}">
<span class="flex-shrink-0 text-xs text-zinc-600 w-10 text-right" aria-hidden="true">{{.Number}}</span>
<span class="text-sm {{if eq .Number $.ChapterN}}text-amber-400 font-medium{{else}}text-zinc-300{{end}}">
{{if .Title}}{{.Title}}{{else}}Chapter {{.Number}}{{end}}
</span>
{{if eq .Number $.ChapterN}}<span class="ml-auto flex-shrink-0 text-[0.7rem] text-amber-500" aria-hidden="true">&#9654; now</span>{{end}}
</a>
{{end}}
</div>
</div>
<!-- Settings dropdown panel -->
<!-- Settings panel -->
<div id="settings-panel"
style="display:none;position:absolute;right:max(0.5rem,calc(50% - 32rem + 0.5rem));top:calc(100% + 0.25rem);min-width:260px;background:#18181b;border:1px solid #27272a;border-radius:0.75rem;padding:1rem;box-shadow:0 8px 24px rgba(0,0,0,0.5);z-index:100;">
<!-- Voice -->
<label style="display:block;margin-bottom:0.875rem;">
<span style="display:block;font-size:0.75rem;color:#71717a;margin-bottom:0.375rem;">Voice</span>
role="dialog"
aria-label="Reader settings"
hidden
class="absolute right-[max(0.5rem,calc(50%-32rem+0.5rem))] top-[calc(100%+0.25rem)] min-w-[260px] bg-zinc-900 border border-zinc-800 rounded-xl p-4 shadow-2xl z-[100]">
<label class="block mb-3.5">
<span class="block text-xs text-zinc-500 mb-1.5">Voice</span>
<select id="tts-voice"
style="width:100%;border-radius:0.5rem;background:#27272a;border:1px solid #3f3f46;padding:0.375rem 0.5rem;font-size:0.875rem;color:#e4e4e7;outline:none;">
class="w-full rounded-lg bg-zinc-800 border border-zinc-700 px-2 py-1.5 text-sm text-zinc-200 outline-none">
{{range .Voices}}
<option value="{{.}}"{{if eq . $.DefaultVoice}} selected{{end}}>{{.}}</option>
{{end}}
</select>
</label>
<!-- Speed -->
<label style="display:block;margin-bottom:0.875rem;">
<span style="display:block;font-size:0.75rem;color:#71717a;margin-bottom:0.375rem;">Speed — <span id="tts-speed-label">1.0×</span></span>
<label class="block mb-3.5">
<span class="block text-xs text-zinc-500 mb-1.5">Speed — <span id="tts-speed-label">1.0×</span></span>
<input id="tts-speed" type="range"
min="0.5" max="2" step="0.1" value="1"
style="width:100%;accent-color:#f59e0b;cursor:pointer;" />
class="w-full accent-amber-500 cursor-pointer" />
</label>
<!-- Auto-next -->
<label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;user-select:none;">
<input id="tts-autoplay" type="checkbox" style="accent-color:#f59e0b;cursor:pointer;width:1rem;height:1rem;" />
<span style="font-size:0.875rem;color:#d4d4d8;">Auto-play next chapter</span>
<label class="flex items-center gap-2 cursor-pointer select-none">
<input id="tts-autoplay" type="checkbox" class="accent-amber-500 cursor-pointer w-4 h-4" />
<span class="text-sm text-zinc-300">Auto-play next chapter</span>
</label>
</div>
</nav>
<!-- Audio element outside nav/header so Reader Mode never sees it -->
<audio id="tts-audio" aria-hidden="true" style="display:none"></audio>
<!-- Hidden audio element -->
<audio id="tts-audio" aria-hidden="true" hidden></audio>
<!-- ─── Chapter content ───────────────────────────────────────────────────── -->
<!-- role="main" + <article> give Reader Mode a strong content anchor -->
<div role="main" class="max-w-2xl mx-auto px-4 py-10">
<!-- ─── Chapter content ───────────────────────────────────────────────────── -->
<main class="max-w-2xl mx-auto px-4 py-10">
<!-- Visible h1 inside the content area — Reader Mode anchors on this title -->
<h1 class="text-2xl font-bold text-zinc-100 mb-6 leading-snug">{{.Title}}</h1>
<article id="chapter-article" class="prose text-zinc-300 leading-relaxed text-[1.05rem]">
@@ -1974,69 +1980,68 @@ const chapterTmpl = `
</a>
{{else}}<span></span>{{end}}
</nav>
</div>
</main>
<!-- ─── Audio queue panel (sticky bottom) ──────────────────────────────────── -->
<div id="audio-queue-bar"
role="region"
aria-label="Audio queue"
style="position:fixed;bottom:0;left:0;right:0;z-index:60;background:#09090b;border-top:1px solid #27272a;font-size:0.75rem;color:#a1a1aa;">
<!-- ─── Audio queue bar (sticky bottom) ──────────────────────────────────── -->
<aside id="audio-queue-bar"
aria-label="Audio queue"
class="fixed bottom-0 left-0 right-0 z-60 bg-zinc-950 border-t border-zinc-800 text-xs text-zinc-400">
<!-- Toggle handle ─────────────────────────────────────────────────────────── -->
<!-- Toggle handle -->
<button id="queue-toggle"
type="button"
onclick="window.toggleQueuePanel()"
aria-expanded="false"
style="width:100%;display:flex;align-items:center;justify-content:between;gap:0.5rem;padding:0.4rem 1rem;background:transparent;border:none;cursor:pointer;color:#a1a1aa;font-size:0.7rem;text-align:left;">
<span style="flex:1;display:flex;align-items:center;gap:0.5rem;">
<span id="queue-toggle-icon" style="font-size:0.65rem;">&#9650;</span>
<span style="font-weight:600;letter-spacing:0.05em;text-transform:uppercase;">Audio Queue</span>
class="w-full flex items-center gap-2 px-4 py-1.5 bg-transparent border-none cursor-pointer text-zinc-400 text-[0.7rem] text-left">
<span class="flex-1 flex items-center gap-2">
<span id="queue-toggle-icon" aria-hidden="true" class="text-[0.65rem]">&#9650;</span>
<span class="font-semibold tracking-widest uppercase">Audio Queue</span>
<span id="queue-now-badge" class="queue-badge queue-badge-idle">idle</span>
</span>
<!-- Mini scrubber always visible in the handle row -->
<span style="flex:2;display:flex;align-items:center;gap:0.5rem;padding:0 0.5rem;">
<span id="queue-time-cur" style="min-width:2.5rem;text-align:right;font-variant-numeric:tabular-nums;">0:00</span>
<span style="flex:1;position:relative;height:3px;background:#3f3f46;border-radius:2px;overflow:hidden;">
<span id="queue-scrubber-fill" style="position:absolute;left:0;top:0;height:100%;width:0%;background:#f59e0b;transition:width 0.3s linear;"></span>
<!-- Mini scrubber -->
<span class="flex-[2] flex items-center gap-2 px-2" aria-hidden="true">
<span id="queue-time-cur" class="min-w-[2.5rem] text-right tabular-nums">0:00</span>
<span class="flex-1 relative h-[3px] bg-zinc-700 rounded-sm overflow-hidden">
<span id="queue-scrubber-fill" class="absolute left-0 top-0 h-full w-0 bg-amber-500 transition-[width] duration-300 linear"></span>
</span>
<span id="queue-time-tot" style="min-width:2.5rem;font-variant-numeric:tabular-nums;">0:00</span>
<span id="queue-time-tot" class="min-w-[2.5rem] tabular-nums">0:00</span>
</span>
</button>
<!-- Expanded panel ────────────────────────────────────────────────────────── -->
<div id="queue-panel-body" style="display:none;border-top:1px solid #27272a;">
<!-- Expanded panel -->
<div id="queue-panel-body" hidden class="border-t border-zinc-800">
<!-- Now playing row -->
<!-- Now playing -->
<div class="queue-row" id="queue-row-current">
<span class="queue-row-label">Now</span>
<span class="queue-row-title" id="queue-cur-title">—</span>
<span class="queue-badge" id="queue-cur-badge">idle</span>
</div>
<!-- Up next row (hidden if no next chapter) -->
<div class="queue-row" id="queue-row-next" style="display:none;">
<!-- Up next -->
<div class="queue-row" id="queue-row-next" hidden>
<span class="queue-row-label">Next</span>
<span class="queue-row-title" id="queue-next-title">—</span>
<span class="queue-badge" id="queue-next-badge">—</span>
</div>
<!-- Debug info row -->
<!-- Debug row -->
<div class="queue-row queue-row-debug" id="queue-row-debug">
<span class="queue-row-label">dbg</span>
<span id="queue-debug-text" style="font-family:monospace;word-break:break-all;"></span>
<span id="queue-debug-text" class="font-mono break-all"></span>
</div>
</div>
</div>
</aside>
<style>
/* Paragraph TTS highlight — no cursor change so paragraphs look editorial */
/* Paragraph TTS highlight */
#chapter-article p {
border-radius: 0.375rem;
margin-left: -0.5rem;
padding-left: 0.5rem;
transition: background 0.15s;
}
/* Subtle tap affordance only on touch/pointer devices that support hover */
@media (hover: hover) {
#chapter-article p:hover { background: rgba(251,191,36,0.07); }
}
@@ -2045,16 +2050,13 @@ const chapterTmpl = `
border-left: 2px solid #f59e0b;
padding-left: calc(0.5rem - 2px);
}
/* Prevent sticky nav from covering anchor targets */
#main-content { scroll-padding-top: 3.5rem; }
/* Improve base reading typography */
/* Prevent sticky nav from obscuring anchor targets */
#main-content { scroll-padding-top: 3.5rem; padding-bottom: 4rem; }
#chapter-article { font-size: 1.0625rem; line-height: 1.8; }
#chapter-article p + p { margin-top: 0; }
/* ── Audio queue bar ─────────────────────────────────────────────────────── */
/* Extra bottom padding so chapter text is never hidden behind the sticky bar */
#main-content { padding-bottom: 4rem; }
/* Audio queue badges */
.queue-badge {
display: inline-block;
padding: 0.1em 0.5em;
@@ -2065,13 +2067,14 @@ const chapterTmpl = `
text-transform: uppercase;
white-space: nowrap;
}
.queue-badge-idle { background: #27272a; color: #71717a; }
.queue-badge-generating { background: #78350f; color: #fbbf24; }
.queue-badge-ready { background: #14532d; color: #4ade80; }
.queue-badge-playing { background: #1e3a5f; color: #60a5fa; }
.queue-badge-paused { background: #1c1917; color: #d6d3d1; }
.queue-badge-error { background: #450a0a; color: #f87171; }
.queue-badge-idle { background: #27272a; color: #71717a; }
.queue-badge-generating { background: #78350f; color: #fbbf24; }
.queue-badge-ready { background: #14532d; color: #4ade80; }
.queue-badge-playing { background: #1e3a5f; color: #60a5fa; }
.queue-badge-paused { background: #1c1917; color: #d6d3d1; }
.queue-badge-error { background: #450a0a; color: #f87171; }
/* Queue rows */
.queue-row {
display: flex;
align-items: center;
@@ -2095,11 +2098,7 @@ const chapterTmpl = `
white-space: nowrap;
color: #d4d4d8;
}
.queue-row-debug {
background: #0c0c0e;
color: #52525b;
font-size: 0.65rem;
}
.queue-row-debug { background: #0c0c0e; color: #52525b; font-size: 0.65rem; }
</style>
<script>