feat: chapter list drawer in sticky header + remove duplicate title heading
- Remove the duplicate <h1> that appeared in the content area below the sticky bar (title is already shown in the header) - Replace the static title span with a clickable button (title + ▼ caret) that opens a full-width chapter list drawer below the sticky bar - Drawer is server-rendered with all chapters; current chapter is highlighted in amber and scrolled into view on open; clicking any row navigates to it and closes the drawer - Opening the chapter list closes the settings panel and vice versa - Both panels dismiss on outside click - Pass AllChapters to the chapter template data struct
This commit is contained in:
@@ -1075,8 +1075,14 @@ const chapterTmpl = `
|
||||
<span style="flex-shrink:0;padding:0.375rem 0.625rem;border-radius:0.5rem;color:#52525b;font-size:0.8125rem;cursor:default;">← Prev</span>
|
||||
{{end}}
|
||||
|
||||
<!-- Chapter title (centre, truncated) -->
|
||||
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center;font-size:0.8125rem;color:#a1a1aa;" title="{{.Title}}">{{.Title}}</span>
|
||||
<!-- Chapter title button — opens chapter list drawer -->
|
||||
<button id="chapter-list-btn"
|
||||
onclick="toggleChapterList()"
|
||||
title="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.375rem 0.25rem;">
|
||||
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.8125rem;color:#a1a1aa;">{{.Title}}</span>
|
||||
<span style="flex-shrink:0;font-size:0.625rem;color:#52525b;">▼</span>
|
||||
</button>
|
||||
|
||||
<!-- Next chapter -->
|
||||
{{if .NextN}}
|
||||
@@ -1119,6 +1125,28 @@ const chapterTmpl = `
|
||||
<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;">
|
||||
{{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"
|
||||
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;">{{.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;">▶ now</span>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings dropdown 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;">
|
||||
@@ -1152,10 +1180,6 @@ const chapterTmpl = `
|
||||
<!-- ─── Chapter content ────────────────────────────────────────────────────── -->
|
||||
<div class="max-w-2xl mx-auto px-4 py-10">
|
||||
|
||||
<header>
|
||||
<h1 class="text-2xl font-bold text-zinc-100 mb-6">{{.Title}}</h1>
|
||||
</header>
|
||||
|
||||
<!-- Paragraphs get data-para-idx injected by JS after render -->
|
||||
<article id="chapter-article" class="prose text-zinc-300 leading-relaxed text-[1.05rem]">
|
||||
{{.HTML}}
|
||||
@@ -1237,20 +1261,50 @@ const chapterTmpl = `
|
||||
var article = document.getElementById('chapter-article');
|
||||
|
||||
// ── settings panel toggle ─────────────────────────────────────────────────────
|
||||
var settingsPanel = document.getElementById('settings-panel');
|
||||
var settingsPanel = document.getElementById('settings-panel');
|
||||
var chapterListPanel = document.getElementById('chapter-list-panel');
|
||||
var chapterListBtn = document.getElementById('chapter-list-btn');
|
||||
|
||||
window.closeChapterList = function () {
|
||||
chapterListPanel.style.display = 'none';
|
||||
chapterListBtn.querySelector('span:last-child').innerHTML = '▼';
|
||||
};
|
||||
|
||||
window.toggleChapterList = function () {
|
||||
var open = chapterListPanel.style.display !== 'none';
|
||||
// Close settings if open
|
||||
settingsPanel.style.display = 'none';
|
||||
if (open) {
|
||||
closeChapterList();
|
||||
} else {
|
||||
chapterListPanel.style.display = 'block';
|
||||
chapterListBtn.querySelector('span:last-child').innerHTML = '▲';
|
||||
// Scroll the current chapter row into view inside the panel
|
||||
var current = chapterListPanel.querySelector('[data-current="1"]');
|
||||
if (current) current.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
};
|
||||
|
||||
window.toggleSettings = function () {
|
||||
var open = settingsPanel.style.display !== 'none';
|
||||
// Close chapter list if open
|
||||
closeChapterList();
|
||||
settingsPanel.style.display = open ? 'none' : 'block';
|
||||
};
|
||||
|
||||
// Close settings panel when clicking outside
|
||||
// Close both panels when clicking outside
|
||||
document.addEventListener('click', function (e) {
|
||||
if (settingsPanel.style.display === 'none') return;
|
||||
var btn = document.getElementById('settings-btn');
|
||||
if (!settingsPanel.contains(e.target) && e.target !== btn && !btn.contains(e.target)) {
|
||||
var settingsBtn = document.getElementById('settings-btn');
|
||||
if (settingsPanel.style.display !== 'none' &&
|
||||
!settingsPanel.contains(e.target) &&
|
||||
e.target !== settingsBtn && !settingsBtn.contains(e.target)) {
|
||||
settingsPanel.style.display = 'none';
|
||||
}
|
||||
if (chapterListPanel.style.display !== 'none' &&
|
||||
!chapterListPanel.contains(e.target) &&
|
||||
e.target !== chapterListBtn && !chapterListBtn.contains(e.target)) {
|
||||
closeChapterList();
|
||||
}
|
||||
});
|
||||
|
||||
function setStatus(text) {
|
||||
@@ -1848,6 +1902,7 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {
|
||||
NextN int
|
||||
ChapterN int
|
||||
Title string
|
||||
AllChapters interface{}
|
||||
KokoroURL string
|
||||
Voices []string
|
||||
DefaultVoice string
|
||||
@@ -1858,6 +1913,7 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {
|
||||
NextN: nextN,
|
||||
ChapterN: n,
|
||||
Title: title,
|
||||
AllChapters: chapters,
|
||||
KokoroURL: s.kokoroURL,
|
||||
Voices: kokoroVoices,
|
||||
DefaultVoice: s.kokoroVoice,
|
||||
|
||||
Reference in New Issue
Block a user