Add basic animation
This commit is contained in:
@@ -17,7 +17,9 @@ const image = await findImage(post.image);
|
||||
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
|
||||
---
|
||||
|
||||
<article class="mb-6 transition">
|
||||
<article
|
||||
class="mb-6 transition intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||
>
|
||||
<div class="relative md:h-64 bg-gray-400 dark:bg-slate-700 rounded shadow-lg mb-6">
|
||||
{
|
||||
image &&
|
||||
|
@@ -21,7 +21,9 @@ const image = (await findImage(post.image)) as ImageMetadata | undefined;
|
||||
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
|
||||
---
|
||||
|
||||
<article class={`max-w-md mx-auto md:max-w-none grid gap-6 md:gap-8 ${image ? 'md:grid-cols-2' : ''}`}>
|
||||
<article
|
||||
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' : ''}`}
|
||||
>
|
||||
{
|
||||
image &&
|
||||
(link ? (
|
||||
|
@@ -18,7 +18,10 @@ const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : [];
|
||||
{
|
||||
APP_BLOG.isRelatedPostsEnabled ? (
|
||||
<BlogHighlightedPosts
|
||||
classes={{ container: 'pt-0 lg:pt-0 md:pt-0' }}
|
||||
classes={{
|
||||
container:
|
||||
'pt-0 lg:pt-0 md:pt-0 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
|
||||
}}
|
||||
title="Related Posts"
|
||||
linkText="View All Posts"
|
||||
linkUrl={getBlogPermalink()}
|
||||
|
@@ -20,7 +20,11 @@ const { post, url } = Astro.props;
|
||||
|
||||
<section class="py-8 sm:py-16 lg:py-20 mx-auto">
|
||||
<article>
|
||||
<header class={post.image ? '' : ''}>
|
||||
<header
|
||||
class={post.image
|
||||
? 'intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade'
|
||||
: 'intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade'}
|
||||
>
|
||||
<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">
|
||||
<p>
|
||||
<Icon name="tabler:clock" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
|
||||
|
@@ -160,3 +160,122 @@ import { UI } from 'astrowind:config';
|
||||
onPageShow();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script is:inline>
|
||||
/* Inspired by: https://github.com/heidkaemper/tailwindcss-intersect */
|
||||
const Observer = {
|
||||
observers: {},
|
||||
visibleElementsQueue: [],
|
||||
processing: false,
|
||||
delayBetweenAnimations: 100,
|
||||
|
||||
start() {
|
||||
const selectors = [
|
||||
'[class*=" intersect:"]',
|
||||
'[class*=":intersect:"]',
|
||||
'[class^="intersect:"]',
|
||||
'[class="intersect"]',
|
||||
'[class*=" intersect "]',
|
||||
'[class^="intersect "]',
|
||||
'[class$=" intersect"]',
|
||||
];
|
||||
|
||||
const elements = Array.from(document.querySelectorAll(selectors.join(',')));
|
||||
|
||||
elements.forEach((el) => el.setAttribute('no-intersect', ''));
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
Object.values(this.observers).forEach((observer) => observer.disconnect());
|
||||
this.observers = {};
|
||||
|
||||
const callback = (entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const target = entry.target;
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
if (target.classList.contains('intercept-no-queue')) {
|
||||
target.removeAttribute('no-intersect');
|
||||
if (target.classList.contains('intersect-once')) {
|
||||
Object.values(this.observers).forEach((observer) => observer.unobserve(target));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.visibleElementsQueue.includes(target)) {
|
||||
this.visibleElementsQueue.push(target);
|
||||
}
|
||||
|
||||
this.processQueue();
|
||||
} else {
|
||||
target.setAttribute('no-intersect', '');
|
||||
|
||||
const index = this.visibleElementsQueue.indexOf(target);
|
||||
if (index > -1) {
|
||||
this.visibleElementsQueue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
elements.forEach((el) => {
|
||||
const threshold = getThreshold(el);
|
||||
|
||||
if (!this.observers[threshold]) {
|
||||
this.observers[threshold] = new IntersectionObserver(callback, { threshold });
|
||||
}
|
||||
|
||||
this.observers[threshold].observe(el);
|
||||
});
|
||||
},
|
||||
|
||||
async processQueue() {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
|
||||
while (this.visibleElementsQueue.length > 0) {
|
||||
const element = this.visibleElementsQueue.shift();
|
||||
|
||||
element.removeAttribute('no-intersect');
|
||||
|
||||
if (element.classList.contains('intersect-once')) {
|
||||
Object.values(this.observers).forEach((observer) => observer.unobserve(element));
|
||||
}
|
||||
|
||||
if (this.isElementInViewport(element)) {
|
||||
await this.delay(this.delayBetweenAnimations);
|
||||
}
|
||||
}
|
||||
|
||||
this.processing = false;
|
||||
},
|
||||
|
||||
delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
},
|
||||
|
||||
isElementInViewport(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return (
|
||||
rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.bottom > 0 &&
|
||||
rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
|
||||
rect.right > 0
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
Observer.start();
|
||||
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
Observer.start();
|
||||
});
|
||||
</script>
|
||||
|
@@ -33,7 +33,7 @@ const {
|
||||
)}
|
||||
>
|
||||
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
|
||||
<div>
|
||||
<div class="intersect-once motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
|
||||
<div class={twMerge('flex flex-row max-w-md', panelClass, itemClasses?.panel)}>
|
||||
<div class="flex justify-center">
|
||||
{(icon || defaultIcon) && (
|
||||
|
@@ -33,7 +33,13 @@ const {
|
||||
)}
|
||||
>
|
||||
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
|
||||
<div class={twMerge('relative flex flex-col', panelClass, itemClasses?.panel)}>
|
||||
<div
|
||||
class={twMerge(
|
||||
'relative flex flex-col intersect-once intersect-quarter intercept-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
|
||||
panelClass,
|
||||
itemClasses?.panel
|
||||
)}
|
||||
>
|
||||
{(icon || defaultIcon) && (
|
||||
<Icon name={icon || defaultIcon} class={twMerge('mb-2 w-10 h-10', defaultIconClass, itemClasses?.icon)} />
|
||||
)}
|
||||
|
@@ -24,7 +24,13 @@ const {
|
||||
items && items.length && (
|
||||
<div class={containerClass}>
|
||||
{items.map(({ title, description, icon, classes: itemClasses = {} }, index = 0) => (
|
||||
<div class={twMerge('flex', panelClass, itemClasses?.panel)}>
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
|
||||
panelClass,
|
||||
itemClasses?.panel
|
||||
)}
|
||||
>
|
||||
<div class="flex flex-col items-center mr-4 rtl:mr-0 rtl:ml-4">
|
||||
<div>
|
||||
<div class="flex items-center justify-center">
|
||||
|
@@ -22,7 +22,10 @@ const WrapperTag = as;
|
||||
</div>
|
||||
<div
|
||||
class:list={[
|
||||
twMerge('relative mx-auto max-w-7xl px-4 md:px-6 py-12 md:py-16 lg:py-20 text-default', containerClass),
|
||||
twMerge(
|
||||
'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',
|
||||
containerClass
|
||||
),
|
||||
{ dark: isDark },
|
||||
]}
|
||||
>
|
||||
|
@@ -28,7 +28,9 @@ const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme
|
||||
|
||||
<footer class:list={[{ dark: theme === 'dark' }, 'relative border-t border-gray-200 dark:border-slate-800 not-prose']}>
|
||||
<div class="dark:bg-dark absolute inset-0 pointer-events-none" aria-hidden="true"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 dark:text-slate-300">
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<div class="grid grid-cols-12 gap-4 gap-y-8 sm:gap-8 py-8 md:py-12">
|
||||
<div class="col-span-12 lg:col-span-4">
|
||||
<div class="mb-2">
|
||||
|
@@ -31,7 +31,7 @@ const {
|
||||
{
|
||||
tagline && (
|
||||
<p
|
||||
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
|
||||
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"
|
||||
set:html={tagline}
|
||||
/>
|
||||
)
|
||||
@@ -39,16 +39,23 @@ const {
|
||||
{
|
||||
title && (
|
||||
<h1
|
||||
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
|
||||
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"
|
||||
set:html={title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
|
||||
{
|
||||
subtitle && (
|
||||
<p
|
||||
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"
|
||||
set:html={subtitle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
actions && (
|
||||
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4">
|
||||
<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">
|
||||
{Array.isArray(actions) ? (
|
||||
actions.map((action) => (
|
||||
<div class="flex w-full sm:w-auto">
|
||||
@@ -64,7 +71,9 @@ const {
|
||||
</div>
|
||||
{content && <Fragment set:html={content} />}
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="intersect-once intercept-no-queue intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||
>
|
||||
{
|
||||
image && (
|
||||
<div class="relative m-auto max-w-5xl">
|
||||
|
@@ -31,7 +31,7 @@ const {
|
||||
{
|
||||
tagline && (
|
||||
<p
|
||||
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
|
||||
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"
|
||||
set:html={tagline}
|
||||
/>
|
||||
)
|
||||
@@ -39,17 +39,24 @@ const {
|
||||
{
|
||||
title && (
|
||||
<h1
|
||||
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
|
||||
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"
|
||||
set:html={title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div class="max-w-3xl mx-auto lg:max-w-none">
|
||||
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
|
||||
{
|
||||
subtitle && (
|
||||
<p
|
||||
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"
|
||||
set:html={subtitle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
actions && (
|
||||
<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">
|
||||
<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">
|
||||
{Array.isArray(actions) ? (
|
||||
actions.map((action) => (
|
||||
<div class="flex w-full sm:w-auto">
|
||||
@@ -68,7 +75,7 @@ const {
|
||||
<div class="basis-1/2">
|
||||
{
|
||||
image && (
|
||||
<div class="relative m-auto max-w-5xl">
|
||||
<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">
|
||||
{typeof image === 'string' ? (
|
||||
<Fragment set:html={image} />
|
||||
) : (
|
||||
|
@@ -30,7 +30,7 @@ const {
|
||||
{
|
||||
tagline && (
|
||||
<p
|
||||
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
|
||||
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"
|
||||
set:html={tagline}
|
||||
/>
|
||||
)
|
||||
@@ -38,14 +38,23 @@ const {
|
||||
{
|
||||
title && (
|
||||
<h1
|
||||
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
|
||||
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"
|
||||
set:html={title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
|
||||
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4">
|
||||
{
|
||||
subtitle && (
|
||||
<p
|
||||
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"
|
||||
set:html={subtitle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<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"
|
||||
>
|
||||
{
|
||||
callToAction && (
|
||||
<div class="flex w-full sm:w-auto">
|
||||
|
@@ -25,7 +25,7 @@ const {
|
||||
{
|
||||
prices &&
|
||||
prices.map(({ title, subtitle, price, period, items, callToAction, hasRibbon = false, ribbonTitle }) => (
|
||||
<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">
|
||||
<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">
|
||||
{price && period && (
|
||||
<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">
|
||||
{hasRibbon && ribbonTitle && (
|
||||
|
@@ -23,7 +23,7 @@ const {
|
||||
{
|
||||
stats &&
|
||||
stats.map(({ amount, title, icon }) => (
|
||||
<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">
|
||||
<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">
|
||||
{icon && (
|
||||
<div class="flex items-center justify-center mx-auto mb-4 text-primary">
|
||||
<Icon name={icon} class="w-10 h-10" />
|
||||
|
@@ -26,7 +26,7 @@ const {
|
||||
{
|
||||
testimonials &&
|
||||
testimonials.map(({ title, testimonial, name, job, image }) => (
|
||||
<div class="flex h-auto">
|
||||
<div class="flex h-auto intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
|
||||
<div class="flex flex-col p-4 md:p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600">
|
||||
{title && <h2 class="text-lg font-medium leading-6 pb-4">{title}</h2>}
|
||||
{testimonial && (
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||
import plugin from 'tailwindcss/plugin';
|
||||
import typographyPlugin from '@tailwindcss/typography';
|
||||
|
||||
export default {
|
||||
@@ -17,8 +18,24 @@ export default {
|
||||
serif: ['var(--aw-font-serif, ui-serif)', ...defaultTheme.fontFamily.serif],
|
||||
heading: ['var(--aw-font-heading, ui-sans-serif)', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
|
||||
animation: {
|
||||
fade: 'fadeInUp 1s both',
|
||||
},
|
||||
|
||||
keyframes: {
|
||||
fadeInUp: {
|
||||
'0%': { opacity: 0, transform: 'translateY(2rem)' },
|
||||
'100%': { opacity: 1, transform: 'translateY(0)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [typographyPlugin],
|
||||
plugins: [
|
||||
typographyPlugin,
|
||||
plugin(({ addVariant }) => {
|
||||
addVariant('intersect', '&:not([no-intersect])');
|
||||
}),
|
||||
],
|
||||
darkMode: 'class',
|
||||
};
|
||||
|
Reference in New Issue
Block a user