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>
|
<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}}
|
{{end}}
|
||||||
|
|
||||||
<!-- Chapter title (centre, truncated) -->
|
<!-- Chapter title button — opens chapter list drawer -->
|
||||||
<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>
|
<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 -->
|
<!-- Next chapter -->
|
||||||
{{if .NextN}}
|
{{if .NextN}}
|
||||||
@@ -1119,6 +1125,28 @@ const chapterTmpl = `
|
|||||||
<span id="tts-status"></span>
|
<span id="tts-status"></span>
|
||||||
</div>
|
</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 -->
|
<!-- Settings dropdown panel -->
|
||||||
<div id="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;">
|
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 ────────────────────────────────────────────────────── -->
|
<!-- ─── Chapter content ────────────────────────────────────────────────────── -->
|
||||||
<div class="max-w-2xl mx-auto px-4 py-10">
|
<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 -->
|
<!-- Paragraphs get data-para-idx injected by JS after render -->
|
||||||
<article id="chapter-article" class="prose text-zinc-300 leading-relaxed text-[1.05rem]">
|
<article id="chapter-article" class="prose text-zinc-300 leading-relaxed text-[1.05rem]">
|
||||||
{{.HTML}}
|
{{.HTML}}
|
||||||
@@ -1238,19 +1262,49 @@ const chapterTmpl = `
|
|||||||
|
|
||||||
// ── settings panel toggle ─────────────────────────────────────────────────────
|
// ── 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 () {
|
window.toggleSettings = function () {
|
||||||
var open = settingsPanel.style.display !== 'none';
|
var open = settingsPanel.style.display !== 'none';
|
||||||
|
// Close chapter list if open
|
||||||
|
closeChapterList();
|
||||||
settingsPanel.style.display = open ? 'none' : 'block';
|
settingsPanel.style.display = open ? 'none' : 'block';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close settings panel when clicking outside
|
// Close both panels when clicking outside
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
if (settingsPanel.style.display === 'none') return;
|
var settingsBtn = document.getElementById('settings-btn');
|
||||||
var btn = document.getElementById('settings-btn');
|
if (settingsPanel.style.display !== 'none' &&
|
||||||
if (!settingsPanel.contains(e.target) && e.target !== btn && !btn.contains(e.target)) {
|
!settingsPanel.contains(e.target) &&
|
||||||
|
e.target !== settingsBtn && !settingsBtn.contains(e.target)) {
|
||||||
settingsPanel.style.display = 'none';
|
settingsPanel.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
if (chapterListPanel.style.display !== 'none' &&
|
||||||
|
!chapterListPanel.contains(e.target) &&
|
||||||
|
e.target !== chapterListBtn && !chapterListBtn.contains(e.target)) {
|
||||||
|
closeChapterList();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function setStatus(text) {
|
function setStatus(text) {
|
||||||
@@ -1848,6 +1902,7 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {
|
|||||||
NextN int
|
NextN int
|
||||||
ChapterN int
|
ChapterN int
|
||||||
Title string
|
Title string
|
||||||
|
AllChapters interface{}
|
||||||
KokoroURL string
|
KokoroURL string
|
||||||
Voices []string
|
Voices []string
|
||||||
DefaultVoice string
|
DefaultVoice string
|
||||||
@@ -1858,6 +1913,7 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {
|
|||||||
NextN: nextN,
|
NextN: nextN,
|
||||||
ChapterN: n,
|
ChapterN: n,
|
||||||
Title: title,
|
Title: title,
|
||||||
|
AllChapters: chapters,
|
||||||
KokoroURL: s.kokoroURL,
|
KokoroURL: s.kokoroURL,
|
||||||
Voices: kokoroVoices,
|
Voices: kokoroVoices,
|
||||||
DefaultVoice: s.kokoroVoice,
|
DefaultVoice: s.kokoroVoice,
|
||||||
|
|||||||
Reference in New Issue
Block a user