Skip to content

Commit ec7d98b

Browse files
committed
Add per-post section tabs and widen the post column
Roundup posts (News / Techniques / Tools) had 100+ items rendered in a single 720px column. Two changes: - assets/css/extended/custom.css: widen --main-width to min(95vw, 1100px) so the column scales with viewport width up to a readable cap. Drop the post-body font to 0.94rem so the wider column doesn't feel oversized. Style the <details> blocks so the long-tail disclosures stand out. - layouts/partials/extend_footer.html: client-side script that wraps each H2 section in a div and adds a sticky tab strip at the top (Overview | News | Techniques | Tools). Overview shows all sections (default); per-section tabs filter to one. URL hash drives initial state so direct links to e.g. /posts/<slug>/#tools land on that tab.
1 parent 1b53ac9 commit ec7d98b

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

assets/css/extended/custom.css

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/* PaperMod's main column defaults to 720px. Roundup posts are wide-by-nature
2+
(long links + bullet lists), so let them spread up to 1100px while still
3+
capping on ultra-wide displays. */
4+
:root {
5+
--main-width: min(95vw, 1100px);
6+
}
7+
8+
.main {
9+
max-width: calc(var(--main-width) + var(--gap) * 2);
10+
}
11+
12+
/* Slight font drop in the post body so a 1000+ px column doesn't feel
13+
oversized. */
14+
.post-content {
15+
font-size: 0.94rem;
16+
line-height: 1.65;
17+
}
18+
.post-content p { margin: 0.7em 0; }
19+
.post-content blockquote {
20+
margin: 0.5em 0 0.9em;
21+
padding: 0.4em 0.9em;
22+
font-size: 0.92rem;
23+
}
24+
.post-content ul,
25+
.post-content ol { margin: 0.5em 0 0.9em; padding-left: 1.4em; }
26+
.post-content li { margin: 0.25em 0; }
27+
.post-content details {
28+
margin: 1.2em 0;
29+
padding: 0.6em 0.95em;
30+
border: 1px solid var(--border);
31+
border-radius: 6px;
32+
background: var(--code-bg);
33+
}
34+
.post-content details summary {
35+
cursor: pointer;
36+
font-weight: 600;
37+
color: var(--secondary);
38+
user-select: none;
39+
}
40+
.post-content details[open] summary { margin-bottom: 0.6em; }
41+
42+
/* Per-post tab strip (Overview / News / Techniques / Tools).
43+
Built at runtime by extend_footer.html. The strip lives at the top of
44+
the article body; sections are wrapped in .post-section divs that get
45+
shown/hidden on tab click. */
46+
.post-tabs {
47+
display: flex;
48+
flex-wrap: wrap;
49+
gap: 0.4rem;
50+
margin: 1rem 0 1.4rem;
51+
padding-bottom: 0.6rem;
52+
border-bottom: 1px solid var(--border);
53+
position: sticky;
54+
top: 0;
55+
background: var(--theme);
56+
z-index: 5;
57+
}
58+
.post-tab {
59+
background: transparent;
60+
border: 1px solid var(--border);
61+
border-radius: 999px;
62+
padding: 0.32rem 0.95rem;
63+
font-size: 0.85rem;
64+
font-weight: 500;
65+
color: var(--secondary);
66+
cursor: pointer;
67+
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
68+
}
69+
.post-tab:hover {
70+
background: var(--code-bg);
71+
color: var(--primary);
72+
border-color: var(--secondary);
73+
}
74+
.post-tab.active {
75+
background: var(--primary);
76+
color: var(--theme);
77+
border-color: var(--primary);
78+
}
79+
.post-section { /* placeholder; shown/hidden via inline style */ }
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{{- /*
2+
Roundup post tab strip. Looks for the H2 section headings inside
3+
.post-content and wraps each section (h2 + everything until the next h2)
4+
in a div the JS can show/hide. Tab strip: Overview (default, shows all),
5+
one button per section. URL hash drives initial tab; clicking persists.
6+
Quietly no-ops on non-roundup posts (fewer than 2 H2s).
7+
*/ -}}
8+
<script>
9+
(function () {
10+
var post = document.querySelector('.post-content[itemprop="articleBody"]')
11+
|| document.querySelector('.post-content');
12+
if (!post) return;
13+
14+
var headings = post.querySelectorAll(':scope > h2');
15+
if (headings.length < 2) return;
16+
17+
// Group each heading with its sibling content, up to (but not including)
18+
// the next heading.
19+
var groups = [];
20+
for (var i = 0; i < headings.length; i++) {
21+
var h = headings[i];
22+
var stop = headings[i + 1] || null;
23+
var els = [h];
24+
var n = h.nextElementSibling;
25+
while (n && n !== stop) { els.push(n); n = n.nextElementSibling; }
26+
groups.push({ id: h.id || ('section-' + i), label: h.textContent.trim(), elements: els });
27+
}
28+
29+
// Wrap each group in a div so we can show/hide as a unit.
30+
var wrappers = groups.map(function (g) {
31+
var div = document.createElement('div');
32+
div.className = 'post-section';
33+
div.dataset.sectionId = g.id;
34+
var first = g.elements[0];
35+
first.parentNode.insertBefore(div, first);
36+
g.elements.forEach(function (el) { div.appendChild(el); });
37+
return div;
38+
});
39+
40+
// Build the tab strip.
41+
var nav = document.createElement('nav');
42+
nav.className = 'post-tabs';
43+
nav.setAttribute('aria-label', 'Post sections');
44+
45+
var buttons = [];
46+
function makeTab(id, label) {
47+
var b = document.createElement('button');
48+
b.type = 'button';
49+
b.className = 'post-tab';
50+
b.dataset.target = id;
51+
b.textContent = label;
52+
b.addEventListener('click', function () { activate(id, true); });
53+
nav.appendChild(b);
54+
buttons.push(b);
55+
}
56+
makeTab('overview', 'Overview');
57+
groups.forEach(function (g) {
58+
// Trim the visible label to the first noun so the strip stays compact:
59+
// "Techniques and Write-ups" -> "Techniques", "Tools and Exploits" -> "Tools".
60+
var short = g.label.split(/\s+and\s+|\s*&\s*/i)[0].trim();
61+
makeTab(g.id, short);
62+
});
63+
64+
function activate(id, push) {
65+
wrappers.forEach(function (w) {
66+
w.style.display = (id === 'overview' || w.dataset.sectionId === id) ? '' : 'none';
67+
});
68+
buttons.forEach(function (b) {
69+
b.classList.toggle('active', b.dataset.target === id);
70+
});
71+
if (push) {
72+
var newHash = id === 'overview' ? '' : '#' + id;
73+
var target = location.pathname + location.search + newHash;
74+
history.replaceState(null, '', target);
75+
}
76+
}
77+
78+
wrappers[0].parentNode.insertBefore(nav, wrappers[0]);
79+
80+
var validIds = { 'overview': 1 };
81+
groups.forEach(function (g) { validIds[g.id] = 1; });
82+
var initial = location.hash.replace('#', '');
83+
activate(validIds[initial] ? initial : 'overview', false);
84+
})();
85+
</script>

0 commit comments

Comments
 (0)