squash and nuke dev
This commit is contained in:
parent
1f1444eb65
commit
e42985d6e6
93 changed files with 33825 additions and 7830 deletions
69
src/config/styling/animations_helper.ts
Normal file
69
src/config/styling/animations_helper.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
export function initAnimations() {
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
threshold: 0.1,
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("visible");
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
observerOptions
|
||||
);
|
||||
|
||||
const animatedElements = document.querySelectorAll(
|
||||
".animate-fade-up, .stagger-parent"
|
||||
);
|
||||
animatedElements.forEach(el => observer.observe(el));
|
||||
}
|
||||
|
||||
export function initTOCHighlighting() {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
const heading = entry.target.querySelector(
|
||||
"h1, h2, h3, h4, h5, h6"
|
||||
);
|
||||
if (heading) {
|
||||
const id = heading.id;
|
||||
const desktopElement = document.querySelector(
|
||||
`.toc-wrapper li a[href="#${id}"]`
|
||||
);
|
||||
const mobileElement = document.querySelector(
|
||||
`.toc-wrapper-mobile li a[href="#${id}"]`
|
||||
);
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
desktopElement?.parentElement?.classList.add("active");
|
||||
mobileElement?.parentElement?.classList.add("active");
|
||||
} else {
|
||||
desktopElement?.parentElement?.classList.remove(
|
||||
"active"
|
||||
);
|
||||
mobileElement?.parentElement?.classList.remove(
|
||||
"active"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll("section[data-heading-rank]")
|
||||
.forEach(section => {
|
||||
observer.observe(section);
|
||||
});
|
||||
}
|
||||
|
||||
// auto-init on DOMContentLoaded
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initAnimations();
|
||||
initTOCHighlighting();
|
||||
});
|
||||
}
|
||||
260
src/config/styling/marquee.ts
Normal file
260
src/config/styling/marquee.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const container = document.querySelector(
|
||||
".marquee"
|
||||
) as HTMLDivElement;
|
||||
const scroller = document.querySelector(
|
||||
".marquee-content"
|
||||
) as HTMLDivElement;
|
||||
const btnLeft = document.getElementById("marquee-scroll-left");
|
||||
const btnRight = document.getElementById(
|
||||
"marquee-scroll-right"
|
||||
);
|
||||
|
||||
if (!container || !scroller) return;
|
||||
|
||||
const bufferSize = 2;
|
||||
let items = Array.from(
|
||||
scroller.querySelectorAll(".marquee-item")
|
||||
) as HTMLDivElement[];
|
||||
const originalCount = items.length;
|
||||
if (originalCount === 0) return;
|
||||
|
||||
let itemWidth = 0;
|
||||
let sequenceWidth = 0;
|
||||
let targetScrollX = 0;
|
||||
let currentScrollX = 0;
|
||||
let isAnimating = false;
|
||||
let isDown = false;
|
||||
let lastTouchX = 0;
|
||||
let touchVelocity = 0;
|
||||
let lastTouchTime = 0;
|
||||
const smoothFactor = 0.1;
|
||||
const snapThreshold = 0.1;
|
||||
|
||||
// setup clones
|
||||
const setupClones = () => {
|
||||
// remove existing clones
|
||||
scroller.querySelectorAll(".clone").forEach(c => c.remove());
|
||||
|
||||
const originals = Array.from(
|
||||
scroller.querySelectorAll(".marquee-item")
|
||||
) as HTMLDivElement[];
|
||||
|
||||
// add clones after
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
originals.forEach(item => {
|
||||
const clone = item.cloneNode(true) as HTMLDivElement;
|
||||
clone.classList.add("clone");
|
||||
scroller.appendChild(clone);
|
||||
});
|
||||
}
|
||||
|
||||
// add clones before
|
||||
const beforeContainer = document.createDocumentFragment();
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
originals.forEach(item => {
|
||||
const clone = item.cloneNode(true) as HTMLDivElement;
|
||||
clone.classList.add("clone");
|
||||
beforeContainer.appendChild(clone);
|
||||
});
|
||||
}
|
||||
scroller.insertBefore(beforeContainer, scroller.firstChild);
|
||||
|
||||
items = Array.from(
|
||||
scroller.querySelectorAll(".marquee-item")
|
||||
) as HTMLDivElement[];
|
||||
};
|
||||
|
||||
const updateDimensions = () => {
|
||||
itemWidth = container.clientWidth;
|
||||
if (itemWidth === 0) return;
|
||||
|
||||
sequenceWidth = originalCount * itemWidth;
|
||||
|
||||
// standardize width
|
||||
scroller.style.width = `${items.length * itemWidth}px`;
|
||||
items.forEach(item => {
|
||||
item.style.width = `${itemWidth}px`;
|
||||
item.style.flex = `0 0 ${itemWidth}px`;
|
||||
item.style.maxWidth = `${itemWidth}px`;
|
||||
});
|
||||
|
||||
targetScrollX =
|
||||
bufferSize * sequenceWidth +
|
||||
(targetScrollX % sequenceWidth);
|
||||
currentScrollX = targetScrollX;
|
||||
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
||||
};
|
||||
|
||||
const lerp = (start: number, end: number, factor: number) =>
|
||||
start + (end - start) * factor;
|
||||
|
||||
const animate = () => {
|
||||
if (!isDown && Math.abs(touchVelocity) < 0.1) {
|
||||
// snap to nearest item if not interacting and close to one
|
||||
const nearestItemScroll =
|
||||
Math.round(targetScrollX / itemWidth) * itemWidth;
|
||||
if (
|
||||
Math.abs(targetScrollX - nearestItemScroll) <
|
||||
itemWidth * 0.5
|
||||
) {
|
||||
targetScrollX = lerp(
|
||||
targetScrollX,
|
||||
nearestItemScroll,
|
||||
0.1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
currentScrollX = lerp(
|
||||
currentScrollX,
|
||||
targetScrollX,
|
||||
smoothFactor
|
||||
);
|
||||
|
||||
// boundary reset
|
||||
if (currentScrollX > (bufferSize + 1) * sequenceWidth) {
|
||||
currentScrollX -= sequenceWidth;
|
||||
targetScrollX -= sequenceWidth;
|
||||
} else if (
|
||||
currentScrollX <
|
||||
(bufferSize - 1) * sequenceWidth
|
||||
) {
|
||||
currentScrollX += sequenceWidth;
|
||||
targetScrollX += sequenceWidth;
|
||||
}
|
||||
|
||||
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
||||
|
||||
// fade in-out and scale items based on distance from center
|
||||
items.forEach((item, index) => {
|
||||
const itemCenter = index * itemWidth;
|
||||
const distance = Math.abs(currentScrollX - itemCenter);
|
||||
const progress = Math.min(distance / itemWidth, 1); // 0 at center, 1 at edge
|
||||
|
||||
const opacity = 1 - progress;
|
||||
const scale = 1 - progress * 0.1; // scale down as it leaves
|
||||
const yOffset = progress * 20; // slide down as it leaves
|
||||
|
||||
item.style.opacity = opacity.toString();
|
||||
// NOTE: apply transform to the video container specifically
|
||||
// to keep layout stable
|
||||
const content = item.querySelector(
|
||||
".marquee-item-content"
|
||||
) as HTMLElement;
|
||||
if (content) {
|
||||
content.style.transform = `scale(${scale}) translateY(${yOffset}px)`;
|
||||
}
|
||||
});
|
||||
|
||||
const diff = Math.abs(targetScrollX - currentScrollX);
|
||||
const interaction = isDown || Math.abs(touchVelocity) > 0.1;
|
||||
|
||||
if (diff > snapThreshold || interaction) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
isAnimating = false;
|
||||
currentScrollX = targetScrollX;
|
||||
scroller.style.transform = `translateX(-${currentScrollX}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
const startAnimation = () => {
|
||||
if (!isAnimating) {
|
||||
isAnimating = true;
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
// video handling
|
||||
const videos = scroller.querySelectorAll("video");
|
||||
const observerOptions = {
|
||||
root: container,
|
||||
threshold: 0.5,
|
||||
};
|
||||
|
||||
const videoObserver = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
const video = entry.target as HTMLVideoElement;
|
||||
if (entry.isIntersecting) {
|
||||
video.play().catch(() => {}); // Handle potential autoplay blocks
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
videos.forEach(v => {
|
||||
videoObserver.observe(v);
|
||||
v.addEventListener("ended", () => {
|
||||
targetScrollX += itemWidth;
|
||||
startAnimation();
|
||||
});
|
||||
});
|
||||
|
||||
// events
|
||||
btnLeft?.addEventListener("click", () => {
|
||||
targetScrollX -= itemWidth;
|
||||
startAnimation();
|
||||
});
|
||||
|
||||
btnRight?.addEventListener("click", () => {
|
||||
targetScrollX += itemWidth;
|
||||
startAnimation();
|
||||
});
|
||||
|
||||
container.addEventListener(
|
||||
"wheel",
|
||||
e => {
|
||||
e.preventDefault();
|
||||
targetScrollX += e.deltaY;
|
||||
startAnimation();
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
|
||||
container.addEventListener("touchstart", e => {
|
||||
isDown = true;
|
||||
lastTouchX = e.touches[0].clientX;
|
||||
lastTouchTime = Date.now();
|
||||
touchVelocity = 0;
|
||||
});
|
||||
|
||||
container.addEventListener("touchmove", e => {
|
||||
if (!isDown) return;
|
||||
const currentTouchX = e.touches[0].clientX;
|
||||
const deltaX = lastTouchX - currentTouchX;
|
||||
targetScrollX += deltaX * 1.5;
|
||||
|
||||
const now = Date.now();
|
||||
const dt = now - lastTouchTime;
|
||||
if (dt > 0) touchVelocity = deltaX / dt;
|
||||
|
||||
lastTouchX = currentTouchX;
|
||||
lastTouchTime = now;
|
||||
startAnimation();
|
||||
});
|
||||
|
||||
container.addEventListener("touchend", () => {
|
||||
isDown = false;
|
||||
targetScrollX += touchVelocity * 100; // Momentum
|
||||
touchVelocity = 0;
|
||||
startAnimation();
|
||||
});
|
||||
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) {
|
||||
videos.forEach(v => v.pause());
|
||||
}
|
||||
});
|
||||
|
||||
// init
|
||||
setupClones();
|
||||
setTimeout(() => {
|
||||
updateDimensions();
|
||||
container.classList.add("initialized");
|
||||
startAnimation();
|
||||
}, 50);
|
||||
});
|
||||
89
src/config/styling/theme_persistence.ts
Normal file
89
src/config/styling/theme_persistence.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
interface ThemeProps {
|
||||
theme: "light" | "dark";
|
||||
system: "light" | "dark";
|
||||
}
|
||||
|
||||
export const getCurrentTheme = (): ThemeProps => {
|
||||
const isDarkSystem = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
const systemTheme = isDarkSystem ? "dark" : "light";
|
||||
|
||||
if (typeof localStorage !== "undefined") {
|
||||
if (localStorage.theme === "dark") {
|
||||
return { theme: "dark", system: systemTheme };
|
||||
}
|
||||
if (localStorage.theme === "light") {
|
||||
return { theme: "light", system: systemTheme };
|
||||
}
|
||||
}
|
||||
|
||||
return { theme: systemTheme, system: systemTheme };
|
||||
};
|
||||
|
||||
export const updateTheme = (transition = true) => {
|
||||
const theme = getCurrentTheme();
|
||||
const toggle = document.getElementById(
|
||||
"theme-manual-toggle"
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (transition) {
|
||||
document.documentElement.classList.add("changing-theme");
|
||||
document.documentElement.style.setProperty(
|
||||
"--theme-transition",
|
||||
"0.3s var(--ease-in-out)"
|
||||
);
|
||||
} else {
|
||||
document.documentElement.style.removeProperty(
|
||||
"--theme-transition"
|
||||
);
|
||||
}
|
||||
|
||||
if (theme.theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
if (toggle) toggle.checked = true;
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
if (toggle) toggle.checked = false;
|
||||
}
|
||||
|
||||
if (transition) {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.classList.remove(
|
||||
"changing-theme"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const initTheme = () => {
|
||||
const toggle = document.getElementById(
|
||||
"theme-manual-toggle"
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (toggle) {
|
||||
toggle.addEventListener("change", () => {
|
||||
localStorage.theme = toggle.checked ? "dark" : "light";
|
||||
updateTheme();
|
||||
});
|
||||
}
|
||||
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", () => updateTheme());
|
||||
window.addEventListener("storage", () => updateTheme());
|
||||
|
||||
// initial sync
|
||||
updateTheme(false);
|
||||
};
|
||||
|
||||
// auto-init on client
|
||||
if (typeof document !== "undefined") {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initTheme);
|
||||
} else {
|
||||
initTheme();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue