Skip to content

Commit c3d063a

Browse files
authored
Merge pull request #23 from 42devops/feat/layout-magazine-toc-header
feat: upgrade site layout with sticky blurred header, magazine posts grid, sticky TOC, and inline footnotes
2 parents 2468e4f + 7b7f4c5 commit c3d063a

3 files changed

Lines changed: 279 additions & 9 deletions

File tree

_layouts/post.html

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,124 @@ <h1 class="post-title">{{ page.title }}</h1>
4141
</div>
4242
</header>
4343

44-
<div class="post-content">
45-
{{ content }}
44+
<div class="post-layout">
45+
<aside class="post-sidebar" id="post-sidebar">
46+
<div class="toc-wrapper" id="toc-wrapper" style="display: none;">
47+
<div class="toc-header">
48+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" x2="3" y1="6" y2="6"/><line x1="15" x2="3" y1="12" y2="12"/><line x1="17" x2="3" y1="18" y2="18"/></svg>
49+
<span>Contents</span>
50+
</div>
51+
<ul class="toc-list" id="toc-list"></ul>
52+
</div>
53+
</aside>
54+
55+
<div class="post-content" id="post-content">
56+
{{ content }}
57+
</div>
4658
</div>
4759
</article>
4860

61+
<script>
62+
document.addEventListener('DOMContentLoaded', function() {
63+
// 1. Generate Table of Contents (TOC)
64+
const content = document.getElementById('post-content');
65+
const tocWrapper = document.getElementById('toc-wrapper');
66+
const tocList = document.getElementById('toc-list');
67+
68+
if (content && tocList && tocWrapper) {
69+
const headings = content.querySelectorAll('h2, h3');
70+
if (headings.length >= 2) {
71+
tocWrapper.style.display = 'block';
72+
73+
headings.forEach((heading, idx) => {
74+
if (!heading.id) {
75+
heading.id = 'heading-' + idx;
76+
}
77+
78+
const li = document.createElement('li');
79+
li.className = 'toc-item ' + heading.tagName.toLowerCase();
80+
81+
const a = document.createElement('a');
82+
a.href = '#' + heading.id;
83+
a.textContent = heading.textContent.replace(/^#+\s*/, '');
84+
85+
a.addEventListener('click', function(e) {
86+
e.preventDefault();
87+
heading.scrollIntoView({ behavior: 'smooth' });
88+
history.pushState(null, null, '#' + heading.id);
89+
});
90+
91+
li.appendChild(a);
92+
tocList.appendChild(li);
93+
});
94+
95+
// IntersectionObserver for active heading highlight
96+
const observerOptions = {
97+
root: null,
98+
rootMargin: '-80px 0px -60% 0px',
99+
threshold: 0
100+
};
101+
102+
const observer = new IntersectionObserver((entries) => {
103+
entries.forEach(entry => {
104+
if (entry.isIntersecting) {
105+
const id = entry.target.id;
106+
const activeLink = tocList.querySelector(`a[href="#${id}"]`);
107+
if (activeLink) {
108+
tocList.querySelectorAll('a').forEach(link => link.classList.remove('active'));
109+
activeLink.classList.add('active');
110+
}
111+
}
112+
});
113+
}, observerOptions);
114+
115+
headings.forEach(h => observer.observe(h));
116+
}
117+
}
118+
119+
// 2. Dynamic Inline Footnotes Popover
120+
const footnotes = document.querySelectorAll('a.footnote, a[href^="#fn:"], sup[id^="fnref"] a');
121+
footnotes.forEach(fnLink => {
122+
fnLink.addEventListener('click', function(e) {
123+
const href = this.getAttribute('href');
124+
if (!href || !href.startsWith('#')) return;
125+
126+
const targetFn = document.querySelector(href);
127+
if (targetFn) {
128+
e.preventDefault();
129+
130+
// Existing popover toggle
131+
let existing = this.parentNode.querySelector('.footnote-popover');
132+
if (existing) {
133+
existing.remove();
134+
return;
135+
}
136+
137+
// Close other popovers
138+
document.querySelectorAll('.footnote-popover').forEach(p => p.remove());
139+
140+
const popover = document.createElement('div');
141+
popover.className = 'footnote-popover';
142+
143+
const textContent = targetFn.innerText.replace(/|/g, '').trim();
144+
popover.innerHTML = `
145+
<div style="font-size: 0.85rem; line-height: 1.5; color: var(--text-main);">${textContent}</div>
146+
<button class="footnote-close-btn" aria-label="Close">&times;</button>
147+
`;
148+
149+
popover.querySelector('.footnote-close-btn').addEventListener('click', (ev) => {
150+
ev.stopPropagation();
151+
popover.remove();
152+
});
153+
154+
this.parentNode.style.position = 'relative';
155+
this.parentNode.appendChild(popover);
156+
}
157+
});
158+
});
159+
});
160+
</script>
161+
49162
{% if site.related_posts.size > 0 %}
50163
<section style="margin-top: 3rem; margin-bottom: 2.5rem;">
51164
<div class="section-header">

css/screen.css

Lines changed: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,20 @@ body {
106106

107107
/* Navigation & Header */
108108
.site-header {
109+
position: sticky;
110+
top: 0;
111+
z-index: 100;
109112
display: flex;
110113
align-items: center;
111114
justify-content: space-between;
112-
padding: 1rem 0 2rem 0;
115+
padding: 1rem 1.5rem;
113116
margin-bottom: 2rem;
117+
background: rgba(var(--bg-card-rgb, 250, 249, 245), 0.82);
118+
backdrop-filter: blur(12px);
119+
-webkit-backdrop-filter: blur(12px);
114120
border-bottom: 1px solid var(--border-color);
121+
border-radius: 0 0 var(--radius-md) var(--radius-md);
122+
transition: padding var(--transition-normal), background-color var(--transition-normal);
115123
}
116124

117125
.brand-wrapper {
@@ -304,12 +312,40 @@ body {
304312

305313
/* Posts List Modern Cards */
306314
.posts-grid {
307-
display: flex;
308-
flex-direction: column;
309-
gap: 1rem;
315+
display: grid;
316+
grid-template-columns: repeat(2, 1fr);
317+
gap: 1.25rem;
310318
margin-bottom: 3rem;
311319
}
312320

321+
@media (max-width: 768px) {
322+
.posts-grid {
323+
grid-template-columns: 1fr;
324+
}
325+
}
326+
327+
.post-card.featured-card {
328+
grid-column: 1 / -1;
329+
background: linear-gradient(135deg, var(--bg-card) 0%, rgba(var(--bg-card-rgb), 0.95) 100%);
330+
border: 1.5px solid var(--brand-primary);
331+
padding: 2rem;
332+
position: relative;
333+
}
334+
335+
.featured-badge {
336+
display: inline-block;
337+
font-family: var(--font-mono);
338+
font-size: 0.725rem;
339+
font-weight: 600;
340+
text-transform: uppercase;
341+
letter-spacing: 0.08em;
342+
color: var(--brand-primary);
343+
background: var(--brand-light);
344+
padding: 0.2rem 0.6rem;
345+
border-radius: var(--radius-sm);
346+
margin-bottom: 0.85rem;
347+
}
348+
313349
.post-card {
314350
background: var(--bg-card);
315351
border: 1px solid var(--border-color);
@@ -611,7 +647,121 @@ pre code {
611647
color: inherit;
612648
}
613649

614-
/* Footer */
650+
/* Post Article Layout & Sticky TOC */
651+
.post-layout {
652+
display: grid;
653+
grid-template-columns: 220px 1fr;
654+
gap: 2.5rem;
655+
align-items: start;
656+
}
657+
658+
@media (max-width: 992px) {
659+
.post-layout {
660+
grid-template-columns: 1fr;
661+
}
662+
.post-sidebar {
663+
display: none;
664+
}
665+
}
666+
667+
.post-sidebar {
668+
position: sticky;
669+
top: 90px;
670+
max-height: calc(100vh - 120px);
671+
overflow-y: auto;
672+
}
673+
674+
.toc-wrapper {
675+
background: var(--bg-card);
676+
border: 1px solid var(--border-color);
677+
border-radius: var(--radius-md);
678+
padding: 1.1rem 1.25rem;
679+
box-shadow: var(--shadow-sm);
680+
}
681+
682+
.toc-header {
683+
display: flex;
684+
align-items: center;
685+
gap: 0.4rem;
686+
font-family: var(--font-mono);
687+
font-size: 0.8rem;
688+
font-weight: 600;
689+
text-transform: uppercase;
690+
letter-spacing: 0.05em;
691+
color: var(--text-light);
692+
margin-bottom: 0.85rem;
693+
padding-bottom: 0.5rem;
694+
border-bottom: 1px solid var(--border-subtle);
695+
}
696+
697+
.toc-list {
698+
list-style: none;
699+
padding: 0;
700+
margin: 0;
701+
display: flex;
702+
flex-direction: column;
703+
gap: 0.4rem;
704+
}
705+
706+
.toc-item.h3 {
707+
padding-left: 0.85rem;
708+
font-size: 0.825rem;
709+
}
710+
711+
.toc-item a {
712+
font-family: var(--font-sans);
713+
font-size: 0.85rem;
714+
color: var(--text-muted);
715+
text-decoration: none;
716+
line-height: 1.4;
717+
display: block;
718+
transition: color var(--transition-fast);
719+
}
720+
721+
.toc-item a:hover,
722+
.toc-item a.active {
723+
color: var(--brand-primary);
724+
font-weight: 600;
725+
}
726+
727+
/* Dynamic Footnote Popover */
728+
.footnote-popover {
729+
position: absolute;
730+
top: 100%;
731+
left: 0;
732+
z-index: 200;
733+
width: 280px;
734+
background: var(--bg-card);
735+
border: 1px solid var(--border-color);
736+
border-radius: var(--radius-md);
737+
padding: 0.85rem 1rem;
738+
box-shadow: var(--shadow-hover);
739+
margin-top: 0.4rem;
740+
animation: fadeInDown 0.2s cubic-bezier(0.16, 1, 0.3, 1);
741+
}
742+
743+
.footnote-close-btn {
744+
position: absolute;
745+
top: 0.35rem;
746+
right: 0.5rem;
747+
background: none;
748+
border: none;
749+
font-size: 1.1rem;
750+
color: var(--text-light);
751+
cursor: pointer;
752+
line-height: 1;
753+
}
754+
755+
@keyframes fadeInDown {
756+
from {
757+
opacity: 0;
758+
transform: translateY(-6px);
759+
}
760+
to {
761+
opacity: 1;
762+
transform: translateY(0);
763+
}
764+
}
615765
.site-footer {
616766
margin-top: auto;
617767
padding: 2.5rem 0 1rem 0;

index.html

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ <h2 class="section-title">Articles</h2>
2626

2727
<div class="posts-grid" id="posts-container">
2828
{% for post in site.posts %}
29-
<a href="{{ post.url }}" class="post-card" data-categories="{% if post.categories %}{{ post.categories | join: ' ' | downcase }}{% endif %}">
29+
<a href="{{ post.url }}" class="post-card {% if forloop.first %}featured-card{% endif %}" data-categories="{% if post.categories %}{{ post.categories | join: ' ' | downcase }}{% endif %}">
30+
{% if forloop.first %}
31+
<div class="featured-badge">Featured Article</div>
32+
{% endif %}
3033
<div class="post-card-header">
3134
<h3 class="post-card-title">{{ post.title }}</h3>
3235
<span class="post-card-meta">
@@ -36,7 +39,11 @@ <h3 class="post-card-title">{{ post.title }}</h3>
3639

3740
{% if post.excerpt %}
3841
<div class="post-card-snippet">
39-
{{ post.excerpt | strip_html | truncatewords: 24 }}
42+
{% if forloop.first %}
43+
{{ post.excerpt | strip_html | truncatewords: 42 }}
44+
{% else %}
45+
{{ post.excerpt | strip_html | truncatewords: 24 }}
46+
{% endif %}
4047
</div>
4148
{% endif %}
4249

0 commit comments

Comments
 (0)