Merge branch 'main' into migration-astro-v5

This commit is contained in:
André B
2024-12-10 19:08:37 -05:00
committed by GitHub
23 changed files with 249 additions and 60 deletions

6
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@onwidget/astrowind", "name": "@onwidget/astrowind",
"version": "1.0.0-beta.47", "version": "1.0.0-beta.49",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@onwidget/astrowind", "name": "@onwidget/astrowind",
"version": "1.0.0-beta.47", "version": "1.0.0-beta.49",
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.10", "@astrojs/rss": "^4.0.10",
"@astrojs/sitemap": "^3.2.1", "@astrojs/sitemap": "^3.2.1",
@@ -10782,4 +10782,4 @@
} }
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@onwidget/astrowind", "name": "@onwidget/astrowind",
"version": "1.0.0-beta.47", "version": "1.0.0-beta.49",
"description": "AstroWind: A free template using Astro 5.0 and Tailwind CSS. Astro starter theme.", "description": "AstroWind: A free template using Astro 5.0 and Tailwind CSS. Astro starter theme.",
"type": "module", "type": "module",
"private": true, "private": true,

View File

@@ -17,7 +17,9 @@ const image = await findImage(post.image);
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : ''; 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"> <div class="relative md:h-64 bg-gray-400 dark:bg-slate-700 rounded shadow-lg mb-6">
{ {
image && image &&

View File

@@ -21,7 +21,9 @@ const image = (await findImage(post.image)) as ImageMetadata | undefined;
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : ''; 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 && image &&
(link ? ( (link ? (

View File

@@ -18,7 +18,10 @@ const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : [];
{ {
APP_BLOG.isRelatedPostsEnabled ? ( APP_BLOG.isRelatedPostsEnabled ? (
<BlogHighlightedPosts <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" title="Related Posts"
linkText="View All Posts" linkText="View All Posts"
linkUrl={getBlogPermalink()} linkUrl={getBlogPermalink()}

View File

@@ -20,7 +20,11 @@ const { post, url } = Astro.props;
<section class="py-8 sm:py-16 lg:py-20 mx-auto"> <section class="py-8 sm:py-16 lg:py-20 mx-auto">
<article> <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"> <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> <p>
<Icon name="tabler:clock" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" /> <Icon name="tabler:clock" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />

View File

@@ -160,3 +160,96 @@ import { UI } from 'astrowind:config';
onPageShow(); onPageShow();
}); });
</script> </script>
<script is:inline>
/* Inspired by: https://github.com/heidkaemper/tailwindcss-intersect */
const Observer = {
observer: null,
delayBetweenAnimations: 100,
animationCounter: 0,
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(',')));
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;
};
elements.forEach((el) => {
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);
}
} else {
target.setAttribute('no-intersect', '');
}
return;
}
if (intersectionRatio >= threshold) {
if (!target.hasAttribute('data-animated')) {
target.removeAttribute('no-intersect');
target.setAttribute('data-animated', 'true');
const delay = this.animationCounter * this.delayBetweenAnimations;
this.animationCounter++;
target.style.transitionDelay = `${delay}ms`;
target.style.animationDelay = `${delay}ms`;
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 = '';
this.animationCounter = 0;
}
});
});
};
this.observer = new IntersectionObserver(callback.bind(this), { threshold: [0, 0.25, 0.5, 0.99] });
elements.forEach((el) => {
this.observer.observe(el);
});
},
};
Observer.start();
document.addEventListener('astro:after-swap', () => {
Observer.start();
});
</script>

View File

@@ -41,15 +41,12 @@ const _image = await findImage(props.src);
let image: ImageType | undefined = undefined; let image: ImageType | undefined = undefined;
if (typeof _image === 'string') { if (
if ((_image.startsWith('http://') || _image.startsWith('https://')) && isUnpicCompatible(_image)) { typeof _image === 'string' &&
image = await getImagesOptimized(_image, props, unpicOptimizer); (_image.startsWith('http://') || _image.startsWith('https://')) &&
} else { isUnpicCompatible(_image)
image = { ) {
src: _image, image = await getImagesOptimized(_image, props, unpicOptimizer);
attributes: { ...props, src: undefined },
};
}
} else if (_image) { } else if (_image) {
image = await getImagesOptimized(_image, props, astroAsseetsOptimizer); image = await getImagesOptimized(_image, props, astroAsseetsOptimizer);
} }

View File

@@ -33,7 +33,7 @@ const {
)} )}
> >
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => ( {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={twMerge('flex flex-row max-w-md', panelClass, itemClasses?.panel)}>
<div class="flex justify-center"> <div class="flex justify-center">
{(icon || defaultIcon) && ( {(icon || defaultIcon) && (

View File

@@ -33,7 +33,13 @@ const {
)} )}
> >
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => ( {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 || defaultIcon) && (
<Icon name={icon || defaultIcon} class={twMerge('mb-2 w-10 h-10', defaultIconClass, itemClasses?.icon)} /> <Icon name={icon || defaultIcon} class={twMerge('mb-2 w-10 h-10', defaultIconClass, itemClasses?.icon)} />
)} )}

View File

@@ -24,7 +24,13 @@ const {
items && items.length && ( items && items.length && (
<div class={containerClass}> <div class={containerClass}>
{items.map(({ title, description, icon, classes: itemClasses = {} }, index = 0) => ( {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 class="flex flex-col items-center mr-4 rtl:mr-0 rtl:ml-4">
<div> <div>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">

View File

@@ -22,7 +22,10 @@ const WrapperTag = as;
</div> </div>
<div <div
class:list={[ 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 }, { dark: isDark },
]} ]}
> >

View File

@@ -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']}> <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="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="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="col-span-12 lg:col-span-4">
<div class="mb-2"> <div class="mb-2">

View File

@@ -90,7 +90,10 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
<li class={links?.length ? 'dropdown' : ''}> <li class={links?.length ? 'dropdown' : ''}>
{links?.length ? ( {links?.length ? (
<> <>
<button type="button" class="hover:text-link dark:hover:text-white px-4 py-3 flex items-center"> <button
type="button"
class="hover:text-link dark:hover:text-white px-4 py-3 flex items-center whitespace-nowrap"
>
{text}{' '} {text}{' '}
<Icon name="tabler:chevron-down" class="w-3.5 h-3.5 ml-0.5 rtl:ml-0 rtl:mr-0.5 hidden md:inline" /> <Icon name="tabler:chevron-down" class="w-3.5 h-3.5 ml-0.5 rtl:ml-0 rtl:mr-0.5 hidden md:inline" />
</button> </button>
@@ -113,7 +116,7 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
) : ( ) : (
<a <a
class:list={[ class:list={[
'hover:text-link dark:hover:text-white px-4 py-3 flex items-center', 'hover:text-link dark:hover:text-white px-4 py-3 flex items-center whitespace-nowrap',
{ 'aw-link-active': href === currentPath }, { 'aw-link-active': href === currentPath },
]} ]}
href={href} href={href}

View File

@@ -31,7 +31,7 @@ const {
{ {
tagline && ( tagline && (
<p <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} set:html={tagline}
/> />
) )
@@ -39,16 +39,23 @@ const {
{ {
title && ( title && (
<h1 <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} set:html={title}
/> />
) )
} }
<div class="max-w-3xl mx-auto"> <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 && ( 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) ? ( {Array.isArray(actions) ? (
actions.map((action) => ( actions.map((action) => (
<div class="flex w-full sm:w-auto"> <div class="flex w-full sm:w-auto">
@@ -64,7 +71,9 @@ const {
</div> </div>
{content && <Fragment set:html={content} />} {content && <Fragment set:html={content} />}
</div> </div>
<div> <div
class="intersect-once intercept-no-queue intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
>
{ {
image && ( image && (
<div class="relative m-auto max-w-5xl"> <div class="relative m-auto max-w-5xl">

View File

@@ -31,7 +31,7 @@ const {
{ {
tagline && ( tagline && (
<p <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} set:html={tagline}
/> />
) )
@@ -39,17 +39,24 @@ const {
{ {
title && ( title && (
<h1 <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} set:html={title}
/> />
) )
} }
<div class="max-w-3xl mx-auto lg:max-w-none"> <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 && ( 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) ? ( {Array.isArray(actions) ? (
actions.map((action) => ( actions.map((action) => (
<div class="flex w-full sm:w-auto"> <div class="flex w-full sm:w-auto">
@@ -68,7 +75,7 @@ const {
<div class="basis-1/2"> <div class="basis-1/2">
{ {
image && ( 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' ? ( {typeof image === 'string' ? (
<Fragment set:html={image} /> <Fragment set:html={image} />
) : ( ) : (

View File

@@ -30,7 +30,7 @@ const {
{ {
tagline && ( tagline && (
<p <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} set:html={tagline}
/> />
) )
@@ -38,14 +38,23 @@ const {
{ {
title && ( title && (
<h1 <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} set:html={title}
/> />
) )
} }
<div class="max-w-3xl mx-auto"> <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 && ( callToAction && (
<div class="flex w-full sm:w-auto"> <div class="flex w-full sm:w-auto">

View File

@@ -25,7 +25,7 @@ const {
{ {
prices && prices &&
prices.map(({ title, subtitle, price, period, items, callToAction, hasRibbon = false, ribbonTitle }) => ( 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 && ( {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"> <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 && ( {hasRibbon && ribbonTitle && (

View File

@@ -23,7 +23,7 @@ const {
{ {
stats && stats &&
stats.map(({ amount, title, icon }) => ( 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 && ( {icon && (
<div class="flex items-center justify-center mx-auto mb-4 text-primary"> <div class="flex items-center justify-center mx-auto mb-4 text-primary">
<Icon name={icon} class="w-10 h-10" /> <Icon name={icon} class="w-10 h-10" />

View File

@@ -26,7 +26,7 @@ const {
{ {
testimonials && testimonials &&
testimonials.map(({ title, testimonial, name, job, image }) => ( 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"> <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>} {title && <h2 class="text-lg font-medium leading-6 pb-4">{title}</h2>}
{testimonial && ( {testimonial && (

View File

@@ -22,13 +22,16 @@ export interface ImageProps extends Omit<HTMLAttributes<'img'>, 'src'> {
widths?: number[] | null; widths?: number[] | null;
aspectRatio?: string | number | null; aspectRatio?: string | number | null;
objectPosition?: string; objectPosition?: string;
format?: string;
} }
export type ImagesOptimizer = ( export type ImagesOptimizer = (
image: ImageMetadata | string, image: ImageMetadata | string,
breakpoints: number[], breakpoints: number[],
width?: number, width?: number,
height?: number height?: number,
format?: string
) => Promise<Array<{ src: string; width: number }>>; ) => Promise<Array<{ src: string; width: number }>>;
/* ******* */ /* ******* */
@@ -209,17 +212,25 @@ const getBreakpoints = ({
}; };
/* ** */ /* ** */
export const astroAsseetsOptimizer: ImagesOptimizer = async (image, breakpoints, _width, _height) => { export const astroAsseetsOptimizer: ImagesOptimizer = async (
image,
breakpoints,
_width,
_height,
format = undefined
) => {
if (!image) { if (!image) {
return []; return [];
} }
return Promise.all( return Promise.all(
breakpoints.map(async (w: number) => { breakpoints.map(async (w: number) => {
const url = (await getImage({ src: image, width: w, inferSize: true })).src; const result = await getImage({ src: image, width: w, inferSize: true, ...(format ? { format: format } : {}) });
return { return {
src: url, src: result?.src,
width: w, width: result?.attributes?.width ?? w,
height: result?.attributes?.height,
}; };
}) })
); );
@@ -230,7 +241,7 @@ export const isUnpicCompatible = (image: string) => {
}; };
/* ** */ /* ** */
export const unpicOptimizer: ImagesOptimizer = async (image, breakpoints, width, height) => { export const unpicOptimizer: ImagesOptimizer = async (image, breakpoints, width, height, format = undefined) => {
if (!image || typeof image !== 'string') { if (!image || typeof image !== 'string') {
return []; return [];
} }
@@ -242,16 +253,19 @@ export const unpicOptimizer: ImagesOptimizer = async (image, breakpoints, width,
return Promise.all( return Promise.all(
breakpoints.map(async (w: number) => { breakpoints.map(async (w: number) => {
const _height = width && height ? computeHeight(w, width / height) : height;
const url = const url =
transformUrl({ transformUrl({
url: image, url: image,
width: w, width: w,
height: width && height ? computeHeight(w, width / height) : height, height: _height,
cdn: urlParsed.cdn, cdn: urlParsed.cdn,
...(format ? { format: format } : {}),
}) || image; }) || image;
return { return {
src: String(url), src: String(url),
width: w, width: w,
height: _height,
}; };
}) })
); );
@@ -270,6 +284,7 @@ export async function getImagesOptimized(
widths, widths,
layout = 'constrained', layout = 'constrained',
style = '', style = '',
format,
...rest ...rest
}: ImageProps, }: ImageProps,
transform: ImagesOptimizer = () => Promise.resolve([]) transform: ImagesOptimizer = () => Promise.resolve([])
@@ -312,7 +327,7 @@ export async function getImagesOptimized(
let breakpoints = getBreakpoints({ width: width, breakpoints: widths, layout: layout }); let breakpoints = getBreakpoints({ width: width, breakpoints: widths, layout: layout });
breakpoints = [...new Set(breakpoints)].sort((a, b) => a - b); breakpoints = [...new Set(breakpoints)].sort((a, b) => a - b);
const srcset = (await transform(image, breakpoints, Number(width) || undefined, Number(height) || undefined)) const srcset = (await transform(image, breakpoints, Number(width) || undefined, Number(height) || undefined, format))
.map(({ src, width }) => `${src} ${width}w`) .map(({ src, width }) => `${src} ${width}w`)
.join(', '); .join(', ');

View File

@@ -1,4 +1,4 @@
import { getImage } from 'astro:assets'; import { isUnpicCompatible, unpicOptimizer, astroAsseetsOptimizer } from './images-optimization';
import type { ImageMetadata } from 'astro'; import type { ImageMetadata } from 'astro';
import type { OpenGraph } from '@astrolib/seo'; import type { OpenGraph } from '@astrolib/seo';
@@ -64,23 +64,34 @@ export const adaptOpenGraphImages = async (
const adaptedImages = await Promise.all( const adaptedImages = await Promise.all(
images.map(async (image) => { images.map(async (image) => {
if (image?.url) { if (image?.url) {
const resolvedImage = (await findImage(image.url)) as ImageMetadata | undefined; const resolvedImage = (await findImage(image.url)) as ImageMetadata | string | undefined;
if (!resolvedImage) { if (!resolvedImage) {
return { return {
url: '', url: '',
}; };
} }
const _image = await getImage({ let _image;
src: resolvedImage,
alt: 'Placeholder alt', if (
width: image?.width || defaultWidth, typeof resolvedImage === 'string' &&
height: image?.height || defaultHeight, (resolvedImage.startsWith('http://') || resolvedImage.startsWith('https://')) &&
}); isUnpicCompatible(resolvedImage)
) {
_image = (await unpicOptimizer(resolvedImage, [defaultWidth], defaultWidth, defaultHeight, 'jpg'))[0];
} else if (resolvedImage) {
const dimensions =
typeof resolvedImage !== 'string' && resolvedImage?.width <= defaultWidth
? [resolvedImage?.width, resolvedImage?.height]
: [defaultWidth, defaultHeight];
_image = (
await astroAsseetsOptimizer(resolvedImage, [dimensions[0]], dimensions[0], dimensions[1], 'jpg')
)[0];
}
if (typeof _image === 'object') { if (typeof _image === 'object') {
return { return {
url: 'src' in _image && typeof _image.src === 'string' ? String(new URL(_image.src, astroSite)) : 'pepe', url: 'src' in _image && typeof _image.src === 'string' ? String(new URL(_image.src, astroSite)) : '',
width: 'width' in _image && typeof _image.width === 'number' ? _image.width : undefined, width: 'width' in _image && typeof _image.width === 'number' ? _image.width : undefined,
height: 'height' in _image && typeof _image.height === 'number' ? _image.height : undefined, height: 'height' in _image && typeof _image.height === 'number' ? _image.height : undefined,
}; };

View File

@@ -1,7 +1,8 @@
import defaultTheme from 'tailwindcss/defaultTheme'; import defaultTheme from 'tailwindcss/defaultTheme';
import plugin from 'tailwindcss/plugin';
import typographyPlugin from '@tailwindcss/typography'; import typographyPlugin from '@tailwindcss/typography';
module.exports = { export default {
content: ['./src/**/*.{astro,html,js,jsx,json,md,mdx,svelte,ts,tsx,vue}'], content: ['./src/**/*.{astro,html,js,jsx,json,md,mdx,svelte,ts,tsx,vue}'],
theme: { theme: {
extend: { extend: {
@@ -17,8 +18,24 @@ module.exports = {
serif: ['var(--aw-font-serif, ui-serif)', ...defaultTheme.fontFamily.serif], serif: ['var(--aw-font-serif, ui-serif)', ...defaultTheme.fontFamily.serif],
heading: ['var(--aw-font-heading, ui-sans-serif)', ...defaultTheme.fontFamily.sans], 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', darkMode: 'class',
}; };