Add Table of Contents

also refactor CSS
This commit is contained in:
2026-04-20 15:44:05 +02:00
parent 7ce4c02dee
commit 9639a70572
8 changed files with 187 additions and 56 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{{if .Photos}} {{if .Photos}}
<div class="diary-photo-grid"> <div class="photo-grid">
{{range .Photos}} {{range .Photos}}
<a href="{{.URL}}" target="_blank"><img src="{{.URL}}" alt="{{.Name}}" loading="lazy"></a> <a href="{{.URL}}" target="_blank"><img src="{{.URL}}" alt="{{.Name}}" loading="lazy"></a>
{{end}} {{end}}
+6 -8
View File
@@ -1,16 +1,14 @@
{{range .Days}} {{range .Days}}
<div class="diary-section"> <h2 id="{{.ID}}">
<h2>
{{if .URL}}<a href="{{.URL}}">{{.Heading}}</a>{{else}}{{.Heading}}{{end}} {{if .URL}}<a href="{{.URL}}">{{.Heading}}</a>{{else}}{{.Heading}}{{end}}
{{if .EditURL}}<a href="{{.EditURL}}" class="btn btn-small">edit</a>{{end}} {{if .EditURL}}<a href="{{.EditURL}}" class="btn btn-small">edit</a>{{end}}
</h2> </h2>
{{if .Content}}<div class="content">{{.Content}}</div>{{end}} {{if .Content}}{{.Content}}{{end}}
{{if .Photos}} {{if .Photos}}
<div class="diary-photo-grid"> <div class="photo-grid">
{{range .Photos}} {{range .Photos}}
<a href="{{.URL}}" target="_blank"><img src="{{.URL}}" alt="{{.Name}}" loading="lazy"></a> <a href="{{.URL}}" target="_blank"><img src="{{.URL}}" alt="{{.Name}}" loading="lazy"></a>
{{end}} {{end}}
</div>
{{end}}
</div> </div>
{{end}} {{end}}
{{end}}
+4 -5
View File
@@ -1,8 +1,7 @@
<h2 id="months">Months</h2>
{{range .Months}} {{range .Months}}
<div class="diary-section"> <h3 id="{{.ID}}">
<h2 class="diary-heading">
<a href="{{.URL}}">{{.Name}}</a> <a href="{{.URL}}">{{.Name}}</a>
{{if .PhotoCount}}<span class="diary-photo-count">({{.PhotoCount}} photos)</span>{{end}} {{if .PhotoCount}}<span class="muted">({{.PhotoCount}} photos)</span>{{end}}
</h2> </h3>
</div>
{{end}} {{end}}
+2 -1
View File
@@ -61,10 +61,11 @@
<div class="content">{{.Content}}</div> <div class="content">{{.Content}}</div>
{{end}} {{end}}
{{if .SpecialContent}} {{if .SpecialContent}}
<div class="diary">{{.SpecialContent}}</div> <div class="content">{{.SpecialContent}}</div>
{{end}} {{end}}
{{if or .Content .SpecialContent}} {{if or .Content .SpecialContent}}
<script src="/_/content.js"></script> <script src="/_/content.js"></script>
<script src="/_/toc.js"></script>
{{end}} {{end}}
{{if .Content}} {{if .Content}}
<script src="/_/sections.js"></script> <script src="/_/sections.js"></script>
+1 -1
View File
@@ -9,7 +9,7 @@
headings.forEach(function (h, i) { headings.forEach(function (h, i) {
var a = document.createElement('a'); var a = document.createElement('a');
a.href = '?edit&section=' + (i + 1); a.href = '?edit&section=' + (i + 1);
a.className = 'btn btn-small section-edit'; a.className = 'btn btn-small';
a.textContent = 'edit'; a.textContent = 'edit';
h.appendChild(a); h.appendChild(a);
}); });
+102 -33
View File
@@ -325,59 +325,32 @@ textarea {
box-sizing: border-box; box-sizing: border-box;
} }
/* === Diary views === */ /* === Muted text === */
.diary-section { .muted {
margin: 2rem 0;
padding-top: 1.5rem;
border-top: 1px dashed var(--secondary);
}
.diary-section:first-child {
border-top: none;
padding-top: 0;
margin-top: 0;
}
.diary-section h2 {
font-size: 1.2rem;
color: var(--text);
margin-bottom: 0.75rem;
font-weight: normal;
}
.diary-photo-count {
color: var(--text-muted); color: var(--text-muted);
font-size: 0.85rem; font-size: 0.85rem;
} }
.diary-photo-grid { /* === Photo grid === */
.photo-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.4rem; gap: 0.4rem;
margin-top: 0.75rem; margin-top: 0.75rem;
} }
.diary-photo-grid a { .photo-grid a {
display: block; display: block;
line-height: 0; line-height: 0;
} }
.diary-photo-grid img { .photo-grid img {
width: 100%; width: 100%;
height: 140px; height: 140px;
object-fit: cover; object-fit: cover;
display: block; display: block;
} }
.diary-section .content {
margin-bottom: 0.75rem;
}
/* === Section edit links === */
.section-edit {
margin-left: 0.75rem;
}
/* === Empty state === */ /* === Empty state === */
.empty { .empty {
padding: 1rem; padding: 1rem;
@@ -405,7 +378,99 @@ hr {
background: var(--primary-hover); background: var(--primary-hover);
} }
/* === Table of contents === */
.toc {
position: fixed;
top: 1rem;
right: 1rem;
width: 14rem;
max-height: calc(100vh - 2rem);
overflow-y: auto;
border: 1px solid var(--secondary);
background: var(--bg);
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
}
.toc-header {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
border-bottom: 1px dashed var(--secondary);
padding-bottom: 0.25rem;
margin-bottom: 0.4rem;
}
.toc ul {
list-style: none;
margin: 0;
padding: 0;
}
.toc li {
margin: 0.15rem 0;
}
.toc a {
color: var(--link);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.toc a:hover {
color: var(--link-hover);
}
.toc-h2 {
padding-left: 0.8rem;
}
.toc-h3 {
padding-left: 1.6rem;
}
.toc-toggle {
display: none;
}
/* === Responsive === */ /* === Responsive === */
@media (max-width: 1100px) {
.toc-toggle {
display: block;
background: none;
border: 1px solid var(--secondary);
color: var(--text);
font: inherit;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
padding: 0.4rem 0.75rem;
margin: 1rem auto;
width: calc(100% - 2rem);
max-width: 860px;
}
.toc-toggle::before {
content: "▸ ";
color: var(--secondary);
}
.toc-toggle[aria-expanded="true"]::before {
content: "▾ ";
}
.toc {
position: static;
display: none;
width: calc(100% - 2rem);
max-width: 860px;
margin: 0 auto 1rem;
max-height: none;
}
.toc.is-open {
display: block;
}
}
@media (max-width: 600px) { @media (max-width: 600px) {
header { header {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
@@ -416,4 +481,8 @@ hr {
textarea { textarea {
min-height: 50vh; min-height: 50vh;
} }
.toc-toggle,
.toc.is-open {
width: calc(100% - 1.5rem);
}
} }
+57
View File
@@ -0,0 +1,57 @@
(function () {
var content = document.querySelector("main");
if (!content) return;
var headings = content.querySelectorAll("h1, h2, h3");
if (headings.length < 2) return;
var nav = document.createElement("nav");
nav.className = "toc";
var header = document.createElement("div");
header.className = "toc-header";
header.textContent = "Contents";
nav.appendChild(header);
var list = document.createElement("ul");
headings.forEach(function (h) {
if (!h.id) return;
var li = document.createElement("li");
li.className = "toc-" + h.tagName.toLowerCase();
var a = document.createElement("a");
a.href = "#" + h.id;
a.textContent = h.textContent;
li.appendChild(a);
list.appendChild(li);
});
nav.appendChild(list);
var toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "toc-toggle";
toggle.textContent = "Contents";
toggle.setAttribute("aria-expanded", "false");
toggle.addEventListener("click", function () {
var open = nav.classList.toggle("is-open");
toggle.setAttribute("aria-expanded", open ? "true" : "false");
});
var main = document.querySelector("main");
if (main) {
main.parentNode.insertBefore(toggle, main);
main.parentNode.insertBefore(nav, main);
} else {
document.body.appendChild(toggle);
document.body.appendChild(nav);
}
var pageHeader = document.querySelector("header");
function updateTop() {
if (!pageHeader || getComputedStyle(nav).position !== "fixed") return;
var rect = pageHeader.getBoundingClientRect();
nav.style.top = Math.max(8, rect.bottom + 8) + "px";
}
window.addEventListener("scroll", updateTop, { passive: true });
window.addEventListener("resize", updateTop);
updateTop();
})();
+9 -2
View File
@@ -69,12 +69,14 @@ type diaryPhoto struct {
} }
type diaryMonthSummary struct { type diaryMonthSummary struct {
ID string
Name string Name string
URL string URL string
PhotoCount int PhotoCount int
} }
type diaryDaySection struct { type diaryDaySection struct {
ID string
Heading string Heading string
URL string URL string
EditURL string EditURL string
@@ -82,7 +84,10 @@ type diaryDaySection struct {
Photos []diaryPhoto Photos []diaryPhoto
} }
type diaryYearData struct{ Months []diaryMonthSummary } type diaryYearData struct {
Months []diaryMonthSummary
Year int
}
type diaryMonthData struct{ Days []diaryDaySection } type diaryMonthData struct{ Days []diaryDaySection }
type diaryDayData struct{ Photos []diaryPhoto } type diaryDayData struct{ Photos []diaryPhoto }
@@ -191,6 +196,7 @@ func renderDiaryYear(fsPath, urlPath string) template.HTML {
} }
monthDate := time.Date(year, time.Month(monthNum), 1, 0, 0, 0, 0, time.UTC) monthDate := time.Date(year, time.Month(monthNum), 1, 0, 0, 0, 0, time.UTC)
months = append(months, diaryMonthSummary{ months = append(months, diaryMonthSummary{
ID: monthDate.Format("2006-01"),
Name: monthDate.Format("January 2006"), Name: monthDate.Format("January 2006"),
URL: path.Join(urlPath, e.Name()) + "/", URL: path.Join(urlPath, e.Name()) + "/",
PhotoCount: count, PhotoCount: count,
@@ -198,7 +204,7 @@ func renderDiaryYear(fsPath, urlPath string) template.HTML {
} }
var buf bytes.Buffer var buf bytes.Buffer
if err := diaryYearTmpl.Execute(&buf, diaryYearData{Months: months}); err != nil { if err := diaryYearTmpl.Execute(&buf, diaryYearData{Months: months, Year: year}); err != nil {
log.Printf("diary year template: %v", err) log.Printf("diary year template: %v", err)
return "" return ""
} }
@@ -280,6 +286,7 @@ func renderDiaryMonth(fsPath, urlPath string) template.HTML {
} }
sections = append(sections, diaryDaySection{ sections = append(sections, diaryDaySection{
ID: date.Format("2006-01-02"),
Heading: heading, Heading: heading,
URL: dayURL, URL: dayURL,
EditURL: dayURL + "?edit", EditURL: dayURL + "?edit",