Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ All notable changes to this project will be documented in this file.

- **Hugging Face Icon Asset**: Download official Hugging Face SVG logo to `src/assets/images/hugging-face.svg`; replace inline placeholder SVGs in `ProjectCard.astro` and `[slug].astro` with `<img src={hfIcon.src}>`

### Fixed

- **CI Auto-Update Workflows**: Scheduled workflows (`Update Project Stats`, `Update Contributors`) failed with GH013 because `GITHUB_TOKEN` cannot bypass the "Protect default branch" ruleset — replaced with `ADMIN_PAT` secret (repo admin added to ruleset bypass list)
- **Observer Redeclaration on View Transitions**: `const Observer` in `BasicScripts.astro` top-level `is:inline` script caused `SyntaxError: Identifier 'Observer' has already been declared` when Astro client router swapped pages — wrapped in IIFE `(() => { ... })()` to create function scope

## [0.3.0] - 2026-07-03

### Added
Expand Down
208 changes: 105 additions & 103 deletions src/components/common/BasicScripts.astro
Original file line number Diff line number Diff line change
Expand Up @@ -223,133 +223,135 @@ import { translations } from '~/utils/translations';

<script is:inline>
/* Inspired by: https://github.com/heidkaemper/tailwindcss-intersect */
const Observer = {
observer: null,
delayBetweenAnimations: 100,
animationCounter: 0,
elements: null,

start(isNavigation) {
if (this.observer) {
this.observer.disconnect();
}
(() => {
const Observer = {
observer: null,
delayBetweenAnimations: 100,
animationCounter: 0,
elements: null,

start(isNavigation) {
if (this.observer) {
this.observer.disconnect();
}

this.animationCounter = 0;

const selectors = [
'[class*=" intersect:"]',
'[class*=":intersect:"]',
'[class^="intersect:"]',
'[class="intersect"]',
'[class*=" intersect "]',
'[class^="intersect "]',
'[class$=" intersect"]',
];

this.elements = Array.from(document.querySelectorAll(selectors.join(',')));

const getThreshold = (element) => {
if (element.classList.contains('intersect-full')) return 0.99;
if (element.classList.contains('intersect-half')) return 0.5;
if (element.classList.contains('intersect-quarter')) return 0.25;
return 0;
};

this.elements.forEach((el) => {
el.setAttribute('no-intersect', '');
el._intersectionThreshold = getThreshold(el);
});
this.animationCounter = 0;

const selectors = [
'[class*=" intersect:"]',
'[class*=":intersect:"]',
'[class^="intersect:"]',
'[class="intersect"]',
'[class*=" intersect "]',
'[class^="intersect "]',
'[class$=" intersect"]',
];

this.elements = Array.from(document.querySelectorAll(selectors.join(',')));

const getThreshold = (element) => {
if (element.classList.contains('intersect-full')) return 0.99;
if (element.classList.contains('intersect-half')) return 0.5;
if (element.classList.contains('intersect-quarter')) return 0.25;
return 0;
};

if (isNavigation) {
const vh = window.innerHeight;
this.elements.forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.top < vh && rect.bottom > 0) {
el.removeAttribute('no-intersect');
el.setAttribute('data-animated', 'true');
el.style.animationDuration = '0s';
}
el.setAttribute('no-intersect', '');
el._intersectionThreshold = getThreshold(el);
});
}

const callback = (entries) => {
entries.forEach((entry) => {
requestAnimationFrame(() => {
const target = entry.target;
const intersectionRatio = entry.intersectionRatio;
const threshold = target._intersectionThreshold;

if (target.classList.contains('intersect-no-queue')) {
if (entry.isIntersecting) {
target.removeAttribute('no-intersect');
if (target.classList.contains('intersect-once')) {
this.observer.unobserve(target);
if (isNavigation) {
const vh = window.innerHeight;
this.elements.forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.top < vh && rect.bottom > 0) {
el.removeAttribute('no-intersect');
el.setAttribute('data-animated', 'true');
el.style.animationDuration = '0s';
}
});
}

const callback = (entries) => {
entries.forEach((entry) => {
requestAnimationFrame(() => {
const target = entry.target;
const intersectionRatio = entry.intersectionRatio;
const threshold = target._intersectionThreshold;

if (target.classList.contains('intersect-no-queue')) {
if (entry.isIntersecting) {
target.removeAttribute('no-intersect');
if (target.classList.contains('intersect-once')) {
this.observer.unobserve(target);
}
} else {
target.setAttribute('no-intersect', '');
}
} else {
target.setAttribute('no-intersect', '');
return;
}
return;
}

if (intersectionRatio >= threshold) {
if (!target.hasAttribute('data-animated')) {
target.removeAttribute('no-intersect');
target.setAttribute('data-animated', 'true');
target.style.animationDuration = '';
if (intersectionRatio >= threshold) {
if (!target.hasAttribute('data-animated')) {
target.removeAttribute('no-intersect');
target.setAttribute('data-animated', 'true');
target.style.animationDuration = '';

const delay = this.animationCounter * this.delayBetweenAnimations;
this.animationCounter++;
const delay = this.animationCounter * this.delayBetweenAnimations;
this.animationCounter++;

target.style.transitionDelay = `${delay}ms`;
target.style.animationDelay = `${delay}ms`;
target.style.transitionDelay = `${delay}ms`;
target.style.animationDelay = `${delay}ms`;

if (target.classList.contains('intersect-once')) {
this.observer.unobserve(target);
if (target.classList.contains('intersect-once')) {
this.observer.unobserve(target);
}
}
} else {
target.setAttribute('no-intersect', '');
target.removeAttribute('data-animated');
target.style.transitionDelay = '';
target.style.animationDelay = '';
target.style.animationDuration = '';

this.animationCounter = 0;
}
} else {
target.setAttribute('no-intersect', '');
target.removeAttribute('data-animated');
target.style.transitionDelay = '';
target.style.animationDelay = '';
target.style.animationDuration = '';

this.animationCounter = 0;
}
});
});
});
};
};

this.observer = new IntersectionObserver(callback, { threshold: [0, 0.25, 0.5, 0.99] });
this.observer = new IntersectionObserver(callback, { threshold: [0, 0.25, 0.5, 0.99] });

this.elements.forEach((el) => {
if (!el.hasAttribute('data-animated')) {
this.observer.observe(el);
}
});
},
this.elements.forEach((el) => {
if (!el.hasAttribute('data-animated')) {
this.observer.observe(el);
}
});
},

/*
/*
REF: #643;
We need to remove the delay to fix flickering/delay
when toggling the theme. Observer only removes them
after data-animated is gone (out of view).
*/
removeAnimationDelay() {
this.elements.forEach((el) => {
if (el.getAttribute('data-animated') === 'true') {
el.style.transitionDelay = '';
el.style.animationDelay = '';
}
});
},
};
removeAnimationDelay() {
this.elements.forEach((el) => {
if (el.getAttribute('data-animated') === 'true') {
el.style.transitionDelay = '';
el.style.animationDelay = '';
}
});
},
};

Observer.start(false);
Observer.start(false);

document.addEventListener('astro:after-swap', () => {
Observer.start(true);
});
document.addEventListener('astro:after-swap', () => {
Observer.start(true);
});
})();
</script>

<style is:global>
Expand Down
Loading