Add Table of Contents
also refactor CSS
This commit is contained in:
@@ -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}}
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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§ion=' + (i + 1);
|
a.href = '?edit§ion=' + (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
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user