Skip to content

Commit 38db18b

Browse files
committed
Add basic animation
1 parent ad5ea2a commit 38db18b

17 files changed

+216
-27
lines changed

src/components/blog/GridItem.astro

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ const image = await findImage(post.image);
1717
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
1818
---
1919

20-
<article class="mb-6 transition">
20+
<article
21+
class="mb-6 transition intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
22+
>
2123
<div class="relative md:h-64 bg-gray-400 dark:bg-slate-700 rounded shadow-lg mb-6">
2224
{
2325
image &&

src/components/blog/ListItem.astro

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ const image = (await findImage(post.image)) as ImageMetadata | undefined;
2121
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
2222
---
2323

24-
<article class={`max-w-md mx-auto md:max-w-none grid gap-6 md:gap-8 ${image ? 'md:grid-cols-2' : ''}`}>
24+
<article
25+
class={`max-w-md mx-auto md:max-w-none grid gap-6 md:gap-8 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade ${image ? 'md:grid-cols-2' : ''}`}
26+
>
2527
{
2628
image &&
2729
(link ? (

src/components/blog/RelatedPosts.astro

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : [];
1818
{
1919
APP_BLOG.isRelatedPostsEnabled ? (
2020
<BlogHighlightedPosts
21-
classes={{ container: 'pt-0 lg:pt-0 md:pt-0' }}
21+
classes={{
22+
container:
23+
'pt-0 lg:pt-0 md:pt-0 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
24+
}}
2225
title="Related Posts"
2326
linkText="View All Posts"
2427
linkUrl={getBlogPermalink()}

src/components/blog/SinglePost.astro

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ const { post, url } = Astro.props;
2020

2121
<section class="py-8 sm:py-16 lg:py-20 mx-auto">
2222
<article>
23-
<header class={post.image ? '' : ''}>
23+
<header
24+
class={post.image
25+
? 'intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade'
26+
: 'intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade'}
27+
>
2428
<div class="flex justify-between flex-col sm:flex-row max-w-3xl mx-auto mt-0 mb-2 px-4 sm:px-6 sm:items-center">
2529
<p>
2630
<Icon name="tabler:clock" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />

src/components/common/BasicScripts.astro

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,122 @@ import { UI } from 'astrowind:config';
160160
onPageShow();
161161
});
162162
</script>
163+
164+
<script is:inline>
165+
/* Inspired by: https://github.com/heidkaemper/tailwindcss-intersect */
166+
const Observer = {
167+
observers: {},
168+
visibleElementsQueue: [],
169+
processing: false,
170+
delayBetweenAnimations: 100,
171+
172+
start() {
173+
const selectors = [
174+
'[class*=" intersect:"]',
175+
'[class*=":intersect:"]',
176+
'[class^="intersect:"]',
177+
'[class="intersect"]',
178+
'[class*=" intersect "]',
179+
'[class^="intersect "]',
180+
'[class$=" intersect"]',
181+
];
182+
183+
const elements = Array.from(document.querySelectorAll(selectors.join(',')));
184+
185+
elements.forEach((el) => el.setAttribute('no-intersect', ''));
186+
187+
const getThreshold = (element) => {
188+
if (element.classList.contains('intersect-full')) return 0.99;
189+
if (element.classList.contains('intersect-half')) return 0.5;
190+
if (element.classList.contains('intersect-quarter')) return 0.25;
191+
return 0;
192+
};
193+
194+
Object.values(this.observers).forEach((observer) => observer.disconnect());
195+
this.observers = {};
196+
197+
const callback = (entries) => {
198+
entries.forEach((entry) => {
199+
const target = entry.target;
200+
201+
if (entry.isIntersecting) {
202+
if (target.classList.contains('intercept-no-queue')) {
203+
target.removeAttribute('no-intersect');
204+
if (target.classList.contains('intersect-once')) {
205+
Object.values(this.observers).forEach((observer) => observer.unobserve(target));
206+
}
207+
return;
208+
}
209+
210+
if (!this.visibleElementsQueue.includes(target)) {
211+
this.visibleElementsQueue.push(target);
212+
}
213+
214+
this.processQueue();
215+
} else {
216+
target.setAttribute('no-intersect', '');
217+
218+
const index = this.visibleElementsQueue.indexOf(target);
219+
if (index > -1) {
220+
this.visibleElementsQueue.splice(index, 1);
221+
}
222+
}
223+
});
224+
};
225+
226+
elements.forEach((el) => {
227+
const threshold = getThreshold(el);
228+
229+
if (!this.observers[threshold]) {
230+
this.observers[threshold] = new IntersectionObserver(callback, { threshold });
231+
}
232+
233+
this.observers[threshold].observe(el);
234+
});
235+
},
236+
237+
async processQueue() {
238+
if (this.processing) {
239+
return;
240+
}
241+
242+
this.processing = true;
243+
244+
while (this.visibleElementsQueue.length > 0) {
245+
const element = this.visibleElementsQueue.shift();
246+
247+
element.removeAttribute('no-intersect');
248+
249+
if (element.classList.contains('intersect-once')) {
250+
Object.values(this.observers).forEach((observer) => observer.unobserve(element));
251+
}
252+
253+
if (this.isElementInViewport(element)) {
254+
await this.delay(this.delayBetweenAnimations);
255+
}
256+
}
257+
258+
this.processing = false;
259+
},
260+
261+
delay(ms) {
262+
return new Promise((resolve) => setTimeout(resolve, ms));
263+
},
264+
265+
isElementInViewport(element) {
266+
const rect = element.getBoundingClientRect();
267+
return (
268+
rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
269+
rect.bottom > 0 &&
270+
rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
271+
rect.right > 0
272+
);
273+
},
274+
};
275+
276+
Observer.start();
277+
278+
document.addEventListener('astro:after-swap', () => {
279+
Observer.start();
280+
});
281+
</script>

src/components/ui/ItemGrid.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const {
3333
)}
3434
>
3535
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
36-
<div>
36+
<div class="intersect-once motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
3737
<div class={twMerge('flex flex-row max-w-md', panelClass, itemClasses?.panel)}>
3838
<div class="flex justify-center">
3939
{(icon || defaultIcon) && (

src/components/ui/ItemGrid2.astro

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ const {
3333
)}
3434
>
3535
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
36-
<div class={twMerge('relative flex flex-col', panelClass, itemClasses?.panel)}>
36+
<div
37+
class={twMerge(
38+
'relative flex flex-col intersect-once intersect-quarter intercept-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
39+
panelClass,
40+
itemClasses?.panel
41+
)}
42+
>
3743
{(icon || defaultIcon) && (
3844
<Icon name={icon || defaultIcon} class={twMerge('mb-2 w-10 h-10', defaultIconClass, itemClasses?.icon)} />
3945
)}

src/components/ui/Timeline.astro

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ const {
2424
items && items.length && (
2525
<div class={containerClass}>
2626
{items.map(({ title, description, icon, classes: itemClasses = {} }, index = 0) => (
27-
<div class={twMerge('flex', panelClass, itemClasses?.panel)}>
27+
<div
28+
class={twMerge(
29+
'flex intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
30+
panelClass,
31+
itemClasses?.panel
32+
)}
33+
>
2834
<div class="flex flex-col items-center mr-4 rtl:mr-0 rtl:ml-4">
2935
<div>
3036
<div class="flex items-center justify-center">

src/components/ui/WidgetWrapper.astro

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ const WrapperTag = as;
2222
</div>
2323
<div
2424
class:list={[
25-
twMerge('relative mx-auto max-w-7xl px-4 md:px-6 py-12 md:py-16 lg:py-20 text-default', containerClass),
25+
twMerge(
26+
'relative mx-auto max-w-7xl px-4 md:px-6 py-12 md:py-16 lg:py-20 text-default intersect-once intersect-quarter intercept-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
27+
containerClass
28+
),
2629
{ dark: isDark },
2730
]}
2831
>

src/components/widgets/Footer.astro

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme
2828

2929
<footer class:list={[{ dark: theme === 'dark' }, 'relative border-t border-gray-200 dark:border-slate-800 not-prose']}>
3030
<div class="dark:bg-dark absolute inset-0 pointer-events-none" aria-hidden="true"></div>
31-
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 dark:text-slate-300">
31+
<div
32+
class="relative max-w-7xl mx-auto px-4 sm:px-6 dark:text-slate-300 intersect-once intersect-quarter intercept-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
33+
>
3234
<div class="grid grid-cols-12 gap-4 gap-y-8 sm:gap-8 py-8 md:py-12">
3335
<div class="col-span-12 lg:col-span-4">
3436
<div class="mb-2">

src/components/widgets/Hero.astro

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,31 @@ const {
3131
{
3232
tagline && (
3333
<p
34-
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
34+
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
3535
set:html={tagline}
3636
/>
3737
)
3838
}
3939
{
4040
title && (
4141
<h1
42-
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
42+
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
4343
set:html={title}
4444
/>
4545
)
4646
}
4747
<div class="max-w-3xl mx-auto">
48-
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
48+
{
49+
subtitle && (
50+
<p
51+
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
52+
set:html={subtitle}
53+
/>
54+
)
55+
}
4956
{
5057
actions && (
51-
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4">
58+
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
5259
{Array.isArray(actions) ? (
5360
actions.map((action) => (
5461
<div class="flex w-full sm:w-auto">
@@ -64,7 +71,9 @@ const {
6471
</div>
6572
{content && <Fragment set:html={content} />}
6673
</div>
67-
<div>
74+
<div
75+
class="intersect-once intercept-no-queue intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
76+
>
6877
{
6978
image && (
7079
<div class="relative m-auto max-w-5xl">

src/components/widgets/Hero2.astro

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,32 @@ const {
3131
{
3232
tagline && (
3333
<p
34-
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
34+
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"
3535
set:html={tagline}
3636
/>
3737
)
3838
}
3939
{
4040
title && (
4141
<h1
42-
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
42+
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"
4343
set:html={title}
4444
/>
4545
)
4646
}
4747
<div class="max-w-3xl mx-auto lg:max-w-none">
48-
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
48+
{
49+
subtitle && (
50+
<p
51+
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"
52+
set:html={subtitle}
53+
/>
54+
)
55+
}
4956

5057
{
5158
actions && (
52-
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 lg:justify-start lg:m-0 lg:max-w-7xl">
59+
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 lg:justify-start lg:m-0 lg:max-w-7xl intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
5360
{Array.isArray(actions) ? (
5461
actions.map((action) => (
5562
<div class="flex w-full sm:w-auto">
@@ -68,7 +75,7 @@ const {
6875
<div class="basis-1/2">
6976
{
7077
image && (
71-
<div class="relative m-auto max-w-5xl">
78+
<div class="relative m-auto max-w-5xl intersect-once intercept-no-queue motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
7279
{typeof image === 'string' ? (
7380
<Fragment set:html={image} />
7481
) : (

src/components/widgets/HeroText.astro

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,31 @@ const {
3030
{
3131
tagline && (
3232
<p
33-
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
33+
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
3434
set:html={tagline}
3535
/>
3636
)
3737
}
3838
{
3939
title && (
4040
<h1
41-
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
41+
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
4242
set:html={title}
4343
/>
4444
)
4545
}
4646
<div class="max-w-3xl mx-auto">
47-
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
48-
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4">
47+
{
48+
subtitle && (
49+
<p
50+
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
51+
set:html={subtitle}
52+
/>
53+
)
54+
}
55+
<div
56+
class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
57+
>
4958
{
5059
callToAction && (
5160
<div class="flex w-full sm:w-auto">

src/components/widgets/Pricing.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const {
2525
{
2626
prices &&
2727
prices.map(({ title, subtitle, price, period, items, callToAction, hasRibbon = false, ribbonTitle }) => (
28-
<div class="col-span-3 mx-auto flex w-full sm:col-span-1 md:col-span-1 lg:col-span-1 xl:col-span-1">
28+
<div class="col-span-3 mx-auto flex w-full sm:col-span-1 md:col-span-1 lg:col-span-1 xl:col-span-1 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
2929
{price && period && (
3030
<div class="rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow px-6 py-8 flex w-full max-w-sm flex-col justify-between text-center">
3131
{hasRibbon && ribbonTitle && (

src/components/widgets/Stats.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const {
2323
{
2424
stats &&
2525
stats.map(({ amount, title, icon }) => (
26-
<div class="p-4 md:w-1/4 sm:w-1/2 w-full min-w-[220px] text-center md:border-r md:last:border-none dark:md:border-slate-500">
26+
<div class="p-4 md:w-1/4 sm:w-1/2 w-full min-w-[220px] text-center md:border-r md:last:border-none dark:md:border-slate-500 intersect-once motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade intersect-quarter">
2727
{icon && (
2828
<div class="flex items-center justify-center mx-auto mb-4 text-primary">
2929
<Icon name={icon} class="w-10 h-10" />

src/components/widgets/Testimonials.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const {
2626
{
2727
testimonials &&
2828
testimonials.map(({ title, testimonial, name, job, image }) => (
29-
<div class="flex h-auto">
29+
<div class="flex h-auto intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
3030
<div class="flex flex-col p-4 md:p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600">
3131
{title && <h2 class="text-lg font-medium leading-6 pb-4">{title}</h2>}
3232
{testimonial && (

0 commit comments

Comments
 (0)