Skip to content

Commit bb2f491

Browse files
authored
Merge branch 'master' into conductor/shorten-testimonials
2 parents baf315b + 050f474 commit bb2f491

File tree

8 files changed

+408
-0
lines changed

8 files changed

+408
-0
lines changed

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ignore Hugo template files with Go template syntax that prettier cannot parse
2+
layouts/_default/_markup/render-heading.html

assets/css/custom.scss

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3061,3 +3061,175 @@ footer {
30613061
}
30623062
}
30633063
}
3064+
3065+
// Floating Table of Contents
3066+
.floating-toc {
3067+
position: fixed;
3068+
left: 2rem;
3069+
top: 120px;
3070+
width: 260px;
3071+
max-height: calc(100vh - 160px);
3072+
overflow-y: auto;
3073+
z-index: 40;
3074+
opacity: 0;
3075+
visibility: hidden;
3076+
transform: translateX(-20px);
3077+
transition: opacity 0.4s ease, visibility 0.4s ease, transform 0.4s ease;
3078+
3079+
// Show when scrolled past header
3080+
&.visible {
3081+
opacity: 1;
3082+
visibility: visible;
3083+
transform: translateX(0);
3084+
}
3085+
3086+
// Hide on mobile and tablets (adjust to show on larger screens)
3087+
@media (max-width: 1400px) {
3088+
display: none;
3089+
}
3090+
3091+
.toc-nav {
3092+
background: rgba(255, 255, 255, 0.98);
3093+
border: 2px solid #e8e8e8;
3094+
border-radius: 6px;
3095+
padding: 0.9rem 1rem;
3096+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
3097+
}
3098+
3099+
.toc-title {
3100+
font-size: 0.7rem;
3101+
font-weight: 800;
3102+
text-transform: uppercase;
3103+
letter-spacing: 0.05em;
3104+
color: $pine;
3105+
margin-bottom: 0.9rem;
3106+
padding-bottom: 0.6rem;
3107+
border-bottom: 2px solid $highlight;
3108+
}
3109+
3110+
.toc-content {
3111+
3112+
// Hugo generates nested ul/li structure
3113+
ul, ol {
3114+
list-style: none !important;
3115+
list-style-type: none !important;
3116+
padding-left: 0 !important;
3117+
margin-left: 0 !important;
3118+
list-style: none;
3119+
padding-left: 0;
3120+
margin: 0;
3121+
3122+
li {
3123+
list-style: none !important;
3124+
list-style-type: none !important;
3125+
3126+
&::before {
3127+
display: none !important;
3128+
content: none !important;
3129+
}
3130+
3131+
// Add subtle separator between items
3132+
&:not(:last-child) {
3133+
border-bottom: 1px solid #f0f0f0;
3134+
}
3135+
3136+
> a {
3137+
font-weight: 600;
3138+
color: #555;
3139+
text-decoration: none;
3140+
display: block;
3141+
padding: 0.3rem 0.4rem;
3142+
padding-left: calc(0.4rem - 3px);
3143+
border-radius: 4px;
3144+
border-left: 3px solid transparent;
3145+
transition: color 0.2s ease, background 0.2s ease, border-left-color 0.2s ease;
3146+
line-height: 1.25;
3147+
3148+
&:hover {
3149+
color: $highlight;
3150+
background: rgba(0, 224, 224, 0.08);
3151+
text-decoration: none;
3152+
}
3153+
3154+
&.active {
3155+
color: $highlight;
3156+
background: rgba(0, 224, 224, 0.12);
3157+
border-left-color: $highlight;
3158+
}
3159+
}
3160+
3161+
// Hide all nested lists (H3, H4, etc.)
3162+
> ul {
3163+
display: none !important;
3164+
}
3165+
}
3166+
}
3167+
}
3168+
3169+
// Custom scrollbar styling
3170+
&::-webkit-scrollbar {
3171+
width: 5px;
3172+
}
3173+
3174+
&::-webkit-scrollbar-track {
3175+
background: transparent;
3176+
}
3177+
3178+
&::-webkit-scrollbar-thumb {
3179+
background: rgba(0, 224, 224, 0.3);
3180+
border-radius: 3px;
3181+
3182+
&:hover {
3183+
background: rgba(0, 224, 224, 0.5);
3184+
}
3185+
}
3186+
}
3187+
3188+
// Header anchor links for blog posts (H1-H6)
3189+
// Entire heading is clickable with # symbol on the right and underline animation
3190+
.heading-with-anchor {
3191+
.heading-anchor-link {
3192+
color: inherit;
3193+
text-decoration: none;
3194+
display: inline-flex;
3195+
align-items: center;
3196+
position: relative;
3197+
3198+
&:hover {
3199+
text-decoration: none;
3200+
color: inherit;
3201+
}
3202+
3203+
.heading-text {
3204+
position: relative;
3205+
3206+
&::after {
3207+
content: '';
3208+
position: absolute;
3209+
left: 0;
3210+
bottom: -2px;
3211+
width: 0;
3212+
height: 2px;
3213+
background-color: $highlight;
3214+
transition: width 0.3s ease-in-out;
3215+
}
3216+
}
3217+
3218+
.heading-anchor-symbol {
3219+
margin-left: 0.5em;
3220+
color: $highlight;
3221+
opacity: 0;
3222+
transition: opacity 0.2s ease-in-out;
3223+
font-weight: 400;
3224+
font-size: 0.8em;
3225+
}
3226+
3227+
&:hover .heading-anchor-symbol {
3228+
opacity: 0.8;
3229+
}
3230+
3231+
&:hover .heading-text::after {
3232+
width: 100%;
3233+
}
3234+
}
3235+
}

assets/js/floating-toc.js

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* Floating Table of Contents with Scroll Spy
3+
* Highlights the current section as user scrolls through the page
4+
*/
5+
(function () {
6+
"use strict";
7+
8+
// Wait for DOM to be ready
9+
if (document.readyState === "loading") {
10+
document.addEventListener("DOMContentLoaded", initTOC);
11+
} else {
12+
initTOC();
13+
}
14+
15+
function initTOC() {
16+
const toc = document.querySelector(".floating-toc");
17+
if (!toc) return; // Exit if no TOC on page
18+
19+
const tocLinks = toc.querySelectorAll("a");
20+
if (tocLinks.length === 0) return;
21+
22+
// Handle TOC visibility based on scroll position
23+
function updateTOCVisibility() {
24+
const scrollPosition =
25+
window.pageYOffset || document.documentElement.scrollTop;
26+
27+
// Find the article content area to determine when to show TOC
28+
const articleContent = document.querySelector(".singleContent");
29+
30+
if (articleContent) {
31+
// Show TOC once user scrolls past the article start
32+
const articleTop =
33+
articleContent.getBoundingClientRect().top + window.pageYOffset;
34+
const triggerPoint = articleTop - 200; // Show 200px before content
35+
36+
if (scrollPosition > triggerPoint) {
37+
toc.classList.add("visible");
38+
} else {
39+
toc.classList.remove("visible");
40+
}
41+
} else {
42+
// Fallback: show after scrolling 600px (roughly past banner)
43+
if (scrollPosition > 600) {
44+
toc.classList.add("visible");
45+
} else {
46+
toc.classList.remove("visible");
47+
}
48+
}
49+
}
50+
51+
// Initialize visibility on page load
52+
updateTOCVisibility();
53+
54+
// Update visibility on scroll (throttled for performance)
55+
let visibilityTimeout;
56+
window.addEventListener(
57+
"scroll",
58+
function () {
59+
if (!visibilityTimeout) {
60+
visibilityTimeout = setTimeout(function () {
61+
updateTOCVisibility();
62+
visibilityTimeout = null;
63+
}, 50);
64+
}
65+
},
66+
{ passive: true },
67+
);
68+
69+
// Get all H2 heading elements that have IDs (these are TOC targets)
70+
const headings = Array.from(document.querySelectorAll("h2[id]")).filter(
71+
(heading) => {
72+
// Only include headings in the main content area
73+
const singleContent = document.querySelector(".singleContent");
74+
return singleContent && singleContent.contains(heading);
75+
},
76+
);
77+
78+
if (headings.length === 0) return;
79+
80+
// Smooth scroll when clicking TOC links
81+
tocLinks.forEach((link) => {
82+
link.addEventListener("click", function (e) {
83+
const href = this.getAttribute("href");
84+
if (href && href.startsWith("#")) {
85+
e.preventDefault();
86+
const targetId = href.substring(1);
87+
const targetElement = document.getElementById(targetId);
88+
89+
if (targetElement) {
90+
const headerHeight = 100; // Adjust based on your fixed header height
91+
const targetPosition =
92+
targetElement.getBoundingClientRect().top +
93+
window.pageYOffset -
94+
headerHeight;
95+
96+
window.scrollTo({
97+
top: targetPosition,
98+
behavior: "smooth",
99+
});
100+
}
101+
}
102+
});
103+
});
104+
105+
// Intersection Observer for scroll spy
106+
const observerOptions = {
107+
rootMargin: "-100px 0px -66%",
108+
threshold: 0,
109+
};
110+
111+
let activeHeading = null;
112+
113+
const observer = new IntersectionObserver((entries) => {
114+
entries.forEach((entry) => {
115+
if (entry.isIntersecting) {
116+
const id = entry.target.id;
117+
activeHeading = id;
118+
updateActiveTOCLink(id);
119+
}
120+
});
121+
}, observerOptions);
122+
123+
// Observe all headings
124+
headings.forEach((heading) => observer.observe(heading));
125+
126+
// Update active link styling
127+
function updateActiveTOCLink(activeId) {
128+
// Remove active class from all links
129+
tocLinks.forEach((link) => link.classList.remove("active"));
130+
131+
// Add active class to current link
132+
if (activeId) {
133+
const activeLink = toc.querySelector(`a[href="#${activeId}"]`);
134+
if (activeLink) {
135+
activeLink.classList.add("active");
136+
137+
// Scroll the TOC if needed to keep active link visible
138+
const tocNav = toc.querySelector(".toc-nav");
139+
if (tocNav) {
140+
const linkRect = activeLink.getBoundingClientRect();
141+
const navRect = tocNav.getBoundingClientRect();
142+
143+
if (
144+
linkRect.bottom > navRect.bottom ||
145+
linkRect.top < navRect.top
146+
) {
147+
activeLink.scrollIntoView({
148+
block: "nearest",
149+
behavior: "smooth",
150+
});
151+
}
152+
}
153+
}
154+
}
155+
}
156+
157+
// Handle initial scroll position on page load
158+
function setInitialActiveLink() {
159+
const scrollPosition = window.pageYOffset;
160+
let currentHeading = null;
161+
162+
for (let i = headings.length - 1; i >= 0; i--) {
163+
const heading = headings[i];
164+
if (heading.offsetTop <= scrollPosition + 150) {
165+
currentHeading = heading.id;
166+
break;
167+
}
168+
}
169+
170+
if (currentHeading) {
171+
updateActiveTOCLink(currentHeading);
172+
}
173+
}
174+
175+
// Set initial active state
176+
setInitialActiveLink();
177+
178+
// Fallback: throttled scroll event listener for browsers with poor Intersection Observer support
179+
let activeLinkTimeout;
180+
window.addEventListener(
181+
"scroll",
182+
function () {
183+
if (activeLinkTimeout) {
184+
window.cancelAnimationFrame(activeLinkTimeout);
185+
}
186+
187+
activeLinkTimeout = window.requestAnimationFrame(function () {
188+
setInitialActiveLink();
189+
});
190+
},
191+
{ passive: true },
192+
);
193+
}
194+
})();

config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ markup:
4646
hardWraps: false
4747
unsafe: true
4848
xhtml: false
49+
tableOfContents:
50+
startLevel: 2
51+
endLevel: 2
52+
ordered: false
4953
menu:
5054
main:
5155
- name: Home

0 commit comments

Comments
 (0)