diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a98f86a..9e8801b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,27 @@ # 5.5.0 -- Ajout du réglage `is_lcp` à `get_the_image()` (fetchpriority high, decoding sync, pas de lazy loading) +- Ajout du réglage `is_lcp` à `get_the_image()` (`fetchpriority` élevé, pas de lazy loading ; `decoding` en opt-in via les attributs) - Ne jamais produire `fetchpriority="high"` avec `loading="lazy"` dans les helpers d’images -- Ajout des helpers `get_theme_static_image_markup()` : images statiques du thème avec width/height (CLS) et conservation de `alt=""` pour les images décoratives -- Abaissement du `fetchpriority` du logo personnalisé pour ne pas concurrencer l’image LCP -- Ajout du filtre `bea_theme_framework_preload_fonts` (vide par défaut) pour précharger les polices above-the-fold -- Réécriture de `Animation` avec des IntersectionObservers partagés au lieu d’une boucle au scroll (mêmes options et callbacks) +- Ajout des helpers `get_theme_static_image_markup()` / `the_theme_static_image()` : images statiques du thème avec `width` / `height` (CLS) et conservation de `alt=""` pour les images décoratives +- Stockage des dimensions des images statiques dans `dist/assets.json` au build (`imageDimensions`) pour éviter les lectures disque répétées à la requête +- Ajout du filtre `bea_theme_framework_preload_fonts` (vide par défaut) pour précharger les polices affichées au premier rendu +- Réécriture de `Animation` avec des `IntersectionObserver` partagés au lieu d’une boucle au scroll (mêmes options et callbacks) - Ajout de l’utilitaire JS `deferInitialization()` pour exécuter du code non critique à idle (INP) -- Documentation de la base Core Web Vitals dans le README +- Documentation de la base Core Web Vitals dans le README (dont compatibilité ARI / `is_lcp`) # 5.3.2 - Mise à jour de CHANGELOG.md et package.json avec le numéro de version BFF le plus récent - Indentation par tabulations imposée dans la base de code - Remplacement de toutes les règles dépréciées des dernières évolutions Dart Sass -- Ajout d’une option d’attribut role dans le service link +- Ajout d’une option d’attribut `role` dans le service link - Suppression du contenu personnalisé du fichier index.php -- Suppression du package browsersync -- Améliorations a11y (skip link, bloc image, bloc tableau) +- Suppression du package BrowserSync +- Améliorations a11y (lien d’évitement, bloc image, bloc tableau) - Ajout du lien de partage Bluesky -- Suppression du custom JS each function -- Ajout de la logique de paramètre URL pour désactiver les animations JS/CSS -- Mise à jour de la version du package oneloop js +- Suppression du custom JS « each » +- Ajout de la logique de paramètre d’URL pour désactiver les animations JS/CSS +- Mise à jour de la version du package oneloop.js - Passage de Node 20.11.0 à 22.13.0 - Ajout du comportement logique RTL - Mise à jour des règles CSS ACF vers Gutenberg @@ -43,7 +43,7 @@ # 5.3.0 et 5.3.1 - Correction de l’exemple de namespace -- Injection de données dans get_terms_list +- Injection de données dans `get_terms_list` # 5.2.1 @@ -52,14 +52,14 @@ # 5.2.0 - Mise à jour des packages npm @wordpress -- Refactor des scripts éditeur pour utiliser les packages @wordpress +- Refactorisation des scripts éditeur pour utiliser les packages @wordpress - Mise à jour de l’enregistrement des assets avec le nouveau mécanisme asset data - Ajout d’une nouvelle méthode pour charger les données - Ajout et configuration du package dependency-extraction-webpack-plugin # 5.1.6 -- Mise à jour des packages npm postcss +- Mise à jour des packages npm PostCSS # 5.1.5 @@ -76,7 +76,7 @@ # 5.1.2 - Mise à jour du package npm canvas -- Correction du script webpack des tailles d’images +- Correction du script Webpack des tailles d’images # 5.1.1 @@ -102,12 +102,12 @@ # 5.0.6 -- Ajout de helpers de templating (the_image, the_text, the_link, etc.) +- Ajout de helpers de templating (`the_image`, `the_text`, `the_link`, etc.) # 5.0.0 - Ajout de Webpack 5 -- Déplacement des fichiers de configuration build dans le dossier config +- Déplacement des fichiers de configuration build dans le dossier `config` - Suppression des scripts bash - Optimisation des images côté front - Suppression des templates statiques @@ -122,5 +122,5 @@ - Gestion du JS backend éditeur - Gestion du CSS frontend/backend éditeur - Version PHP minimale 7.2 -- Suppression de loadFont.php et utilisation de l’enqueue de scripts classique -- Ajout de esc_attr sur defer/async script +- Suppression de `loadFont.php` et utilisation de l’enqueue de scripts classique +- Ajout de `esc_attr` sur les scripts defer/async diff --git a/README.md b/README.md index 72aa7755..1ae9a2db 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ BFF inclut une configuration [wp-env](https://developer.wordpress.org/block-edit Vous pouvez lancer wp-env sans installation globale via `npx @wordpress/env`, ou installer la CLI une fois et utiliser la commande `wp-env` : ```bash -# Global install (npm or yarn) +# Installation globale (npm ou yarn) npm install -g @wordpress/env # ou yarn global add @wordpress/env @@ -198,21 +198,31 @@ yarn bundle-report ### Ce que le thème fournit -- **Image LCP** : passez `'is_lcp' => true` dans les réglages de `get_the_image()` pour l’image principale above-the-fold (hero, couverture). Cela retire `loading="lazy"` et ajoute `fetchpriority="high"` et `decoding="sync"`. À utiliser sur une seule image par page. +- **Image LCP** : passez `'is_lcp' => true` dans les réglages de `get_the_image()` pour l’image principale au-dessus de la ligne de flottaison (hero, couverture). Cela retire `loading="lazy"` et ajoute `fetchpriority="high"`. Définissez `decoding` dans `$attributes` uniquement si une mesure le justifie (ex. WebPageTest). Une seule image par page. Par défaut `is_lcp => false` (opt-in). ```php the_image( $image_id, [ 'alt' => '' ], [ 'size' => 'hero', 'is_lcp' => true ] ); ``` -- **Images décoratives** : passez `'alt' => ''` pour produire `alt=""`, afin que les technologies d’assistance ignorent l’image. Omettre `alt` n’est pas équivalent. -- **Images statiques du thème** (logos, placeholders, illustrations depuis `dist/`) : utilisez `get_theme_static_image_markup()` / `the_theme_static_image()`. `width` et `height` sont lus depuis le fichier (SVG ou raster) pour limiter les décalages de mise en page (CLS). +- **ARI (Auto Responsive Images)** : sur les projets avec ARI, ne pas activer `is_lcp` sur les images déjà gérées par ARI (taille / `data-location` au bon format). ARI pilote la priorité et le lazy-load ; `is_lcp` sur la même image est redondant ou incompatible. Réserver `is_lcp` aux projets sans ARI, ou valider manuellement (page hero avec ARI, sans `is_lcp`). + +- **Images décoratives (`alt=""`)** — même objectif, deux helpers selon la source : + - **Médiathèque** (`the_image` / `get_the_image`) : passez `'alt' => ''` explicitement si l’image est décorative. Sinon WordPress reprend l’alt saisi dans le BO. Omettre la clé `alt` n’est pas équivalent à `alt=""`. + - **Fichiers buildés** dans `dist/` (`the_theme_static_image` / `get_theme_static_image_markup`) : décoratif par défaut (`alt=""` si vous ne passez pas `alt`). Indiquez un texte d’alt si l’image porte du sens (ex. logo + nom du site). Ne pas utiliser `the_image` pour ces fichiers (pas d’ID attachment). +- **Images statiques du thème** (placeholders, illustrations dans `dist/`) : le helper statique ajoute aussi `width` et `height` via `dist/assets.json` (`imageDimensions`, généré au build) pour limiter le CLS, sans lecture disque répétée lorsque le manifeste est présent. Hors manifeste : une lecture par chemin et par requête. ```php +// Média (décoratif) +the_image( $image_id, [ 'alt' => '', 'class' => 'card__thumb' ], [ 'size' => 'card' ] ); + +// Asset dist/ (décoratif par défaut) the_theme_static_image( 'dist/images/placeholder.svg', [ 'class' => 'card__image', 'loading' => 'lazy' ] ); + +// Asset dist/ (alt porteur de sens) +the_theme_static_image( 'dist/images/logo.svg', [ 'alt' => get_bloginfo( 'name' ), 'class' => 'header__logo' ] ); ``` -- **Logo personnalisé** : reçoit `fetchpriority="low"` pour ne pas concurrencer la vraie image LCP. -- **Preload des polices** : opt-in, uniquement pour les polices rendues au premier affichage. +- **Preload des polices** : opt-in, uniquement pour les polices rendues au premier affichage. Indiquez le chemin **après build** (ex. polices Fontsource ou custom copiées dans `dist/fonts/`, en `.woff2`). ```php add_filter( 'bea_theme_framework_preload_fonts', fn() => [ 'dist/fonts/my-font-regular.woff2' ] ); @@ -224,12 +234,12 @@ add_filter( 'bea_theme_framework_preload_fonts', fn() => [ 'dist/fonts/my-font-r Cela dépend de l’hébergement, des extensions et du contenu ; ce n’est donc pas inclus dans le thème : -- Cache de page et réglages WP Rocket (Remove Unused CSS, defer / delay JS et leurs exclusions, exclusions lazyload pour l’image LCP, preconnect limités aux tiers chargés tôt). +- Cache de page et réglages WP Rocket (suppression du CSS inutilisé, différer / retarder le JS et leurs exclusions, exclusions du lazyload pour l’image LCP, preconnect limités aux tiers chargés tôt). - `robots.txt` (endpoints techniques et AJAX) et en-têtes `Cache-Control` pour les assets statiques. - Réglages du plugin SEO : URLs canoniques sur pagination personnalisée, doublons de données structurées. - Polices : sous-ensemble des fichiers aux graisses et glyphes réellement utilisés, suppression des fichiers inutiles (italiques, graisses supplémentaires). - Intégrations tierces (vidéos, formulaires, webinars) : chargement à l’interaction ou à l’entrée dans le viewport. -- Assets des plugins : dequeue des scripts et styles front sur les pages qui ne les utilisent pas. +- Assets des extensions : retirer les scripts et styles front (dequeue) sur les pages qui ne les utilisent pas. ## Éditeur WordPress (Gutenberg) @@ -240,13 +250,13 @@ Le filtre `bff_editor_custom_settings` permet de personnaliser les styles et var ```php add_filter( 'bff_editor_custom_settings', 'customize_editor_settings', 10, 1 ); function customize_editor_settings( $settings ) { - // Disable all block styles for Separator block + // Désactiver tous les styles du bloc Séparateur $settings[ 'disableAllBlocksStyles' ] = [ 'core/separator' ]; - // Disable specific block style for Button block + // Désactiver le style outline du bloc Bouton $settings[ 'disabledBlocksStyles' ] = [ 'core/button' => [ 'outline' ] ]; - // Allow only YouTube variation for Embed block + // Autoriser uniquement la variation YouTube du bloc Intégration $settings[ 'allowedBlocksVariations' ] = [ 'core/embed' => [ 'youtube' ] ]; return $settings; diff --git a/config/plugins.js b/config/plugins.js index 8b41c5d0..bbe1a74b 100644 --- a/config/plugins.js +++ b/config/plugins.js @@ -10,6 +10,7 @@ const DependencyExtractionWebpackPlugin = require('@wordpress/dependency-extract const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin const WebpackImageSizesPlugin = require('./webpack-image-sizes-plugin') +const WebpackStaticImageDimensionsPlugin = require('./webpack-static-image-dimensions-plugin') const WebpackThemeJsonPlugin = require('./webpack-theme-json-plugin') const SpriteHashPlugin = require('./webpack-sprite-hash-plugin') const WebpackIconFilesPlugin = require('./webpack-icon-files-plugin') @@ -64,6 +65,7 @@ module.exports = { new WebpackManifestPlugin({ fileName: 'assets.json', }), + new WebpackStaticImageDimensionsPlugin(), ] if (mode === 'production') { diff --git a/config/webpack-static-image-dimensions-plugin.js b/config/webpack-static-image-dimensions-plugin.js new file mode 100644 index 00000000..ed83ee88 --- /dev/null +++ b/config/webpack-static-image-dimensions-plugin.js @@ -0,0 +1,144 @@ +const fs = require('fs') +const path = require('path') + +let sharp + +try { + sharp = require('sharp') +} catch (error) { + sharp = null +} + +const IMAGE_EXT_PATTERN = /\.(png|jpe?g|gif|svg|webp|avif)$/i + +/** + * Parse intrinsic SVG dimensions (width/height attributes, then viewBox). + * + * @param {string} contents SVG file contents. + * @return {{ width: number, height: number }|null} + */ +function getSvgDimensionsFromContents(contents) { + if (!contents) { + return null + } + + const widthMatch = contents.match(/\bwidth="(\d+(?:\.\d+)?)"/) + const heightMatch = contents.match(/\bheight="(\d+(?:\.\d+)?)"/) + + if (widthMatch && heightMatch) { + return { + width: Math.round(Number(widthMatch[1])), + height: Math.round(Number(heightMatch[1])), + } + } + + const viewBoxMatch = contents.match(/\bviewBox="[\d.\s-]+?\s+([\d.]+)\s+([\d.]+)"/) + + if (viewBoxMatch) { + return { + width: Math.max(1, Math.round(Number(viewBoxMatch[1]))), + height: Math.max(1, Math.round(Number(viewBoxMatch[2]))), + } + } + + return null +} + +/** + * Read width/height from a built image file under dist/. + * + * @param {string} filePath Absolute path to the image file. + * @return {Promise<{ width: number, height: number }|null>} + */ +async function getImageDimensions(filePath) { + if (!fs.existsSync(filePath)) { + return null + } + + if (/\.svg$/i.test(filePath)) { + const contents = fs.readFileSync(filePath, 'utf8') + return getSvgDimensionsFromContents(contents) + } + + if (!sharp) { + return null + } + + try { + const metadata = await sharp(filePath).metadata() + + if (!metadata.width || !metadata.height) { + return null + } + + return { + width: metadata.width, + height: metadata.height, + } + } catch (error) { + return null + } +} + +/** + * Webpack plugin: merge static image dimensions into dist/assets.json. + */ +class WebpackStaticImageDimensionsPlugin { + constructor(options = {}) { + this.options = { + outputPath: 'dist', + manifestFile: 'assets.json', + imagesSubdir: 'images', + ...options, + } + } + + apply(compiler) { + compiler.hooks.afterEmit.tapAsync('WebpackStaticImageDimensionsPlugin', async (compilation, callback) => { + try { + const context = compiler.options.context + const outputPath = path.resolve(context, this.options.outputPath) + const manifestPath = path.join(outputPath, this.options.manifestFile) + const imageDimensions = {} + + const assets = compilation.getAssets() + + for (const asset of assets) { + if (!asset.name.startsWith(`${this.options.imagesSubdir}/`) || !IMAGE_EXT_PATTERN.test(asset.name)) { + continue + } + + const filePath = path.join(outputPath, asset.name) + const dimensions = await getImageDimensions(filePath) + + if (!dimensions) { + continue + } + + const themeRelativePath = `dist/${asset.name.replace(/\\/g, '/')}` + imageDimensions[themeRelativePath] = dimensions + } + + let manifest = {} + + if (fs.existsSync(manifestPath)) { + const raw = fs.readFileSync(manifestPath, 'utf8') + const parsed = JSON.parse(raw) + + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + manifest = parsed + } + } + + manifest.imageDimensions = imageDimensions + + fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`) + callback() + } catch (error) { + callback(error) + } + }) + } +} + +module.exports = WebpackStaticImageDimensionsPlugin diff --git a/inc/Helpers/Formatting/Image.php b/inc/Helpers/Formatting/Image.php index 55b49ac2..7c5903cf 100644 --- a/inc/Helpers/Formatting/Image.php +++ b/inc/Helpers/Formatting/Image.php @@ -1,6 +1,8 @@ 'image-size' ] ); * @@ -17,6 +19,8 @@ * @type string $alt Image description for the alt attribute. * @type string $srcset The 'srcset' attribute value. * @type string $sizes The 'sizes' attribute value. + * @type string $fetchpriority The 'fetchpriority' attribute value (e.g. 'high', 'low'). + * @type string $decoding The 'decoding' attribute value (e.g. 'sync', 'async'). * @type string|false $loading The 'loading' attribute value. Passing a value of false * will result in the attribute being omitted for the image. * Defaults to 'lazy', depending on wp_lazy_loading_enabled(). @@ -29,6 +33,9 @@ * @type string $before Optional. Markup to prepend to the image. Default empty. * @type string $after Optional. Markup to append to the image. Default empty. * @type boolean $default Optional. If WP image does not exists, display a default image. + * @type boolean $is_lcp Optional. When true, applies LCP-friendly attributes + * (loading omitted, fetchpriority=high). + * decoding may be set explicitly in $attributes. * * } * @@ -56,11 +63,23 @@ function get_the_image( int $image_id, array $attributes, array $settings = [] ) 'before' => '', 'after' => '', 'default' => false, + 'is_lcp' => false, ] ); $settings = apply_filters( 'bea_theme_framework_the_image_settings', $settings, $image_id, $attributes ); + if ( ! empty( $settings['is_lcp'] ) ) { + // Always win over caller/WP defaults: the LCP image must not be lazy-loaded. + $attributes['loading'] = false; + $attributes['fetchpriority'] = 'high'; + } + + // Never combine fetchpriority=high with loading=lazy (undefined browser behavior). + if ( isset( $attributes['fetchpriority'] ) && 'high' === $attributes['fetchpriority'] ) { + $attributes['loading'] = false; + } + if ( 0 === $image_id && ! $settings['default'] ) { return ''; } @@ -102,6 +121,8 @@ function get_the_image( int $image_id, array $attributes, array $settings = [] ) * @type string $alt Image description for the alt attribute. * @type string $srcset The 'srcset' attribute value. * @type string $sizes The 'sizes' attribute value. + * @type string $fetchpriority The 'fetchpriority' attribute value (e.g. 'high', 'low'). + * @type string $decoding The 'decoding' attribute value (e.g. 'sync', 'async'). * @type string|false $loading The 'loading' attribute value. Passing a value of false * will result in the attribute being omitted for the image. * Defaults to 'lazy', depending on wp_lazy_loading_enabled(). @@ -113,6 +134,7 @@ function get_the_image( int $image_id, array $attributes, array $settings = [] ) * @type string $size Optional. The 'sizes' attribute value. * @type string $before Optional. Markup to prepend to the image. Default empty. * @type string $after Optional. Markup to append to the image. Default empty. + * @type boolean $is_lcp Optional. When true, applies LCP-friendly attributes. * * } * @@ -121,3 +143,190 @@ function get_the_image( int $image_id, array $attributes, array $settings = [] ) function the_image( int $image_id, array $attributes, array $settings = [] ): void { echo get_the_image( $image_id, $attributes, $settings ); } + +/** + * Resolve the public URI for a static theme asset without redundant filesystem checks. + * + * @param string $relative_theme_path Path relative to the theme root. + * + * @return string + */ +function get_theme_static_image_uri( string $relative_theme_path ): string { + static $uri_cache = []; + + $relative_theme_path = ltrim( $relative_theme_path, '/' ); + + if ( isset( $uri_cache[ $relative_theme_path ] ) ) { + return $uri_cache[ $relative_theme_path ]; + } + + if ( get_stylesheet() === get_template() ) { + $uri = get_stylesheet_directory_uri() . '/' . $relative_theme_path; + $uri = apply_filters( 'theme_file_uri', $uri, $relative_theme_path ); + } else { + $uri = get_theme_file_uri( $relative_theme_path ); + } + + $uri_cache[ $relative_theme_path ] = $uri; + + return $uri; +} + +/** + * Read intrinsic dimensions from a static file under the active theme directory. + * + * Uses build-time dimensions from `dist/assets.json` when available; otherwise reads the file once per request. + * + * @usage BEA\Theme\Framework\Helpers\Formatting\Image\get_theme_static_image_dimensions( 'dist/images/logo.svg' ); + * + * @param string $relative_theme_path Path relative to the theme root (e.g. dist/images/logo.svg). + * + * @return array{width:int,height:int}|null Null when the file is missing or dimensions cannot be read. + */ +function get_theme_static_image_dimensions( string $relative_theme_path ): ?array { + static $cache = []; + + $relative_theme_path = ltrim( $relative_theme_path, '/' ); + + if ( array_key_exists( $relative_theme_path, $cache ) ) { + return $cache[ $relative_theme_path ]; + } + + $assets = Framework::get_container()->get_service( 'assets' ); + $from_manifest = $assets->get_static_image_dimensions( $relative_theme_path ); + + if ( null !== $from_manifest ) { + $cache[ $relative_theme_path ] = $from_manifest; + + return $from_manifest; + } + + $from_filesystem = read_theme_static_image_dimensions_from_filesystem( $relative_theme_path ); + $cache[ $relative_theme_path ] = $from_filesystem; + + return $from_filesystem; +} + +/** + * Fallback: read intrinsic dimensions from the theme filesystem. + * + * @param string $relative_theme_path Path relative to the theme root. + * + * @return array{width:int,height:int}|null + */ +function read_theme_static_image_dimensions_from_filesystem( string $relative_theme_path ): ?array { + $file_path = get_theme_file_path( $relative_theme_path ); + + if ( ! is_string( $file_path ) || '' === $file_path || ! is_readable( $file_path ) ) { + return null; + } + + if ( str_ends_with( strtolower( $file_path ), '.svg' ) ) { + $contents = file_get_contents( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local theme SVG asset. + + if ( ! is_string( $contents ) || '' === $contents ) { + return null; + } + + if ( preg_match( '/\bwidth="(\d+(?:\.\d+)?)"/', $contents, $width_match ) + && preg_match( '/\bheight="(\d+(?:\.\d+)?)"/', $contents, $height_match ) ) { + return [ + 'width' => (int) round( (float) $width_match[1] ), + 'height' => (int) round( (float) $height_match[1] ), + ]; + } + + if ( preg_match( '/\bviewBox="[\d.\s-]+?\s+([\d.]+)\s+([\d.]+)"/', $contents, $view_box_match ) ) { + return [ + 'width' => max( 1, (int) round( (float) $view_box_match[1] ) ), + 'height' => max( 1, (int) round( (float) $view_box_match[2] ) ), + ]; + } + + return null; + } + + $size = wp_getimagesize( $file_path ); + if ( ! is_array( $size ) || empty( $size[0] ) || empty( $size[1] ) ) { + return null; + } + + return [ + 'width' => (int) $size[0], + 'height' => (int) $size[1], + ]; +} + +/** + * Build an img tag for a static theme asset, with width and height when known (prevents CLS). + * + * @usage BEA\Theme\Framework\Helpers\Formatting\Image\get_theme_static_image_markup( 'dist/images/logo.svg', [ 'alt' => 'Logo' ] ); + * + * @param string $relative_theme_path Path relative to the theme root. + * @param array $attributes Img attributes. src and alt are set when omitted. + * An empty alt is output as alt="" (decorative image); + * false or null omits the attribute. + * + * @return string Return the markup of the image + */ +function get_theme_static_image_markup( string $relative_theme_path, array $attributes = [] ): string { + $relative_theme_path = ltrim( $relative_theme_path, '/' ); + + $attributes = wp_parse_args( + $attributes, + [ + 'src' => get_theme_static_image_uri( $relative_theme_path ), + 'alt' => '', + ] + ); + + $dimensions = get_theme_static_image_dimensions( $relative_theme_path ); + if ( null !== $dimensions ) { + if ( ! isset( $attributes['width'] ) ) { + $attributes['width'] = (string) $dimensions['width']; + } + if ( ! isset( $attributes['height'] ) ) { + $attributes['height'] = (string) $dimensions['height']; + } + } + + if ( isset( $attributes['fetchpriority'] ) && 'high' === $attributes['fetchpriority'] ) { + $attributes['loading'] = false; + } + + $processor = new \WP_HTML_Tag_Processor( '' ); + if ( ! $processor->next_tag( 'img' ) ) { + return ''; + } + + foreach ( $attributes as $name => $value ) { + $name = (string) $name; + + if ( false === $value || null === $value ) { + $processor->remove_attribute( $name ); + continue; + } + + // Empty alt marks a decorative image; omitting the attribute is not equivalent for AT. + if ( '' === $value && 'alt' !== $name ) { + $processor->remove_attribute( $name ); + continue; + } + + $processor->set_attribute( $name, (string) $value ); + } + + return $processor->get_updated_html(); +} + +/** + * @usage BEA\Theme\Framework\Helpers\Formatting\Image\the_theme_static_image( 'dist/images/logo.svg', [ 'alt' => 'Logo' ] ); + * + * @param string $relative_theme_path Path relative to the theme root. + * @param array $attributes Img attributes, see get_theme_static_image_markup(). + * + * @return void Echo of the image markup + */ +function the_theme_static_image( string $relative_theme_path, array $attributes = [] ): void { + echo get_theme_static_image_markup( $relative_theme_path, $attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes are escaped by WP_HTML_Tag_Processor. +} diff --git a/inc/Services/Assets.php b/inc/Services/Assets.php index 474af5bb..24c54417 100644 --- a/inc/Services/Assets.php +++ b/inc/Services/Assets.php @@ -140,19 +140,19 @@ public function get_min_file( string $type ): string { switch ( $type ) { case 'css': - $file = $assets['app.css']; + $file = $assets['app.css'] ?? null; break; case 'editor.css': - $file = $assets['editor.css']; + $file = $assets['editor.css'] ?? null; break; case 'login': - $file = $assets['login.css']; + $file = $assets['login.css'] ?? null; break; case 'editor.js': - $file = $assets['editor.js']; + $file = $assets['editor.js'] ?? null; break; case 'js': - $file = $assets['app.js']; + $file = $assets['app.js'] ?? null; break; default: $file = null; @@ -160,17 +160,51 @@ public function get_min_file( string $type ): string { } // Custom type - if ( ! empty( $assets[ $type ] ) ) { + if ( null === $file && ! empty( $assets[ $type ] ) && is_string( $assets[ $type ] ) ) { $file = $assets[ $type ]; } - if ( empty( $file ) ) { + if ( empty( $file ) || ! is_string( $file ) ) { return ''; } return $file; } + /** + * Return intrinsic dimensions for a static theme image from the build manifest. + * + * @param string $relative_theme_path Path relative to the theme root (e.g. dist/images/logo.svg). + * + * @return array{width:int,height:int}|null Null when dimensions are not in the manifest. + */ + public function get_static_image_dimensions( string $relative_theme_path ): ?array { + $relative_theme_path = ltrim( $relative_theme_path, '/' ); + $manifest = $this->get_assets_manifest(); + + if ( empty( $manifest['imageDimensions'] ) || ! is_array( $manifest['imageDimensions'] ) ) { + return null; + } + + $dimensions = $manifest['imageDimensions'][ $relative_theme_path ] ?? null; + + if ( ! is_array( $dimensions ) ) { + return null; + } + + $width = isset( $dimensions['width'] ) ? (int) $dimensions['width'] : 0; + $height = isset( $dimensions['height'] ) ? (int) $dimensions['height'] : 0; + + if ( $width <= 0 || $height <= 0 ) { + return null; + } + + return [ + 'width' => $width, + 'height' => $height, + ]; + } + /** * Read and decode the `dist/assets.json` manifest once per request. * @@ -178,7 +212,7 @@ public function get_min_file( string $type ): string { * `stylesheet_uri` filter, editor assets…): memoize the decoded content to * avoid repeated filesystem reads and JSON decoding. * - * @return array Manifest entries, or an empty array if unavailable/invalid. + * @return array Manifest entries, or an empty array if unavailable/invalid. */ private function get_assets_manifest(): array { static $manifest = null; diff --git a/inc/Services/Performance.php b/inc/Services/Performance.php index c0d24ea1..8e6ec43d 100644 --- a/inc/Services/Performance.php +++ b/inc/Services/Performance.php @@ -25,6 +25,7 @@ public function boot( Service_Container $container ): void { * Add hooks for the scripts and styles to hook on */ add_action( 'wp', [ $this, 'register_assets' ] ); + add_action( 'wp_head', [ $this, 'preload_fonts' ], 1 ); } /** @@ -45,6 +46,49 @@ public function register_assets(): void { wp_add_inline_script( 'jquery', $this->passive_touchstart() ); } + /** + * Preload the woff2 fonts used by the above-the-fold content. + * + * Opt-in: the list is empty by default. Only preload fonts that are actually + * rendered on first paint, otherwise they compete with the LCP resource. + * + * @usage add_filter( 'bea_theme_framework_preload_fonts', fn() => [ 'dist/fonts/my-font.woff2' ] ); + * + * @return void + */ + public function preload_fonts(): void { + if ( is_admin() ) { + return; + } + + /** + * Filter the font files to preload, relative to the theme root. + * + * @param string[] $fonts Font paths relative to the theme root. Default empty. + */ + $fonts = apply_filters( 'bea_theme_framework_preload_fonts', [] ); + if ( ! is_array( $fonts ) ) { + return; + } + + foreach ( $fonts as $relative_path ) { + if ( ! is_string( $relative_path ) || '' === $relative_path ) { + continue; + } + + $relative_path = ltrim( $relative_path, '/' ); + $path = get_theme_file_path( $relative_path ); + if ( ! is_readable( $path ) ) { + continue; + } + + printf( + '' . "\n", + esc_url( get_theme_file_uri( $relative_path ) ) + ); + } + } + /** * Add passsive touchstart for performance * @return string diff --git a/package-lock.json b/package-lock.json index dcecc85b..38bf9428 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,14 +72,42 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.27.0", "dev": true, @@ -91,6 +119,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cacheable/memory": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", @@ -403,9 +445,9 @@ } }, "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -840,9 +882,9 @@ } }, "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1305,9 +1347,9 @@ } }, "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2050,7 +2092,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2699,6 +2743,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "dev": true, @@ -2725,12 +2780,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -2778,16 +2837,18 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" }, "engines": { "node": ">= 10.0.0" @@ -2797,25 +2858,24 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", "cpu": [ "arm64" ], @@ -2834,7 +2894,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", "cpu": [ "arm64" ], @@ -2853,9 +2915,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", "cpu": [ "x64" ], @@ -2874,9 +2936,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", "cpu": [ "x64" ], @@ -2895,13 +2957,16 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2916,13 +2981,16 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2937,13 +3005,16 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2958,13 +3029,16 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2979,13 +3053,16 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3000,13 +3077,16 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3021,9 +3101,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", "cpu": [ "arm64" ], @@ -3041,12 +3121,12 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", @@ -3062,25 +3142,18 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10.0.0" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@polka/url": { @@ -3172,20 +3245,12 @@ "license": "MIT" }, "node_modules/@tannin/sprintf": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@tannin/sprintf/-/sprintf-1.3.3.tgz", - "integrity": "sha512-RwARl+hFwhzy0tg9atWcchLFvoQiOh4rrP7uG2N5E4W80BPCUX0ElcUR9St43fxB9EfjsW2df9Qp+UsTbvQDjA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@tannin/sprintf/-/sprintf-1.3.4.tgz", + "integrity": "sha512-Kypdet5L0Hhkfbdkn2QREczNTjCXhiAnouRINyAePy1lZKHD/zcwiqN93HC7i/vVCAuzNAlp+wuT4yOZ5icOSQ==", "dev": true, "license": "MIT" }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/eslint": { "version": "8.56.12", "dev": true, @@ -3518,14 +3583,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@wordpress/blocks/node_modules/uuid": { - "version": "8.3.2", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@wordpress/browserslist-config": { "version": "6.53.0", "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.53.0.tgz", @@ -3747,14 +3804,13 @@ } }, "node_modules/@wordpress/private-apis": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.53.0.tgz", - "integrity": "sha512-xXW2yeBC0n/doFLsv6chEl3Qs5phqmRH7wcUyil+Xvh3msVGzUJxvyo0szmKpQcmoYafASpkQux82HCtNp6jxQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-2.0.0.tgz", + "integrity": "sha512-VmGwebQZoebF5t1xFMXbj6z/dTtAca4d8YaO3IPN2aP8IT+eOrUBvoAthC5BmLPU4JLN5m3r8vbrid3mcli6vw==", "dev": true, "license": "GPL-2.0-or-later", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" } }, "node_modules/@wordpress/redux-routine": { @@ -3787,25 +3843,24 @@ } }, "node_modules/@wordpress/style-runtime": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@wordpress/style-runtime/-/style-runtime-0.9.0.tgz", - "integrity": "sha512-K2T4OfVbqgsp07zi7C6UemlJK7fF78EPmxFYuxSjnbbB14fJcOxX+Tqi/4y24hn9ECahoGjCKHZCY2U2kts8dQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@wordpress/style-runtime/-/style-runtime-0.13.0.tgz", + "integrity": "sha512-P8/7C0NLt+0r6Ks7csJPRUvTgdPccI51AMs05h8/LEJdVtW2k1nSdAgSpgC/lFAsZCPlCeicxjnelJChNRhXZQ==", "dev": true, "license": "GPL-2.0-or-later", "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" + "node": ">=20.10.0" } }, "node_modules/@wordpress/stylelint-config": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-24.2.0.tgz", - "integrity": "sha512-hRAcYVoGAAeh8abfkF5T7d6O2T7jQgf5jRP6HEWmQOqpOFkhenZaOFC6jdoX+t/PxXFUYFV3mzgjWqNa+b1jXw==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-24.3.0.tgz", + "integrity": "sha512-DfuhPR402icUZd/6MyWRm6jwr7tY5Fbtt+sswFVql8Z+95sNe0kdS4vJyAwhRGWNnZkJ1SuQk6ye6nHFu+cm/g==", "dev": true, "license": "MIT", "dependencies": { "@stylistic/stylelint-plugin": "^3.1.3", - "@wordpress/theme": "^1.2.0", + "@wordpress/theme": "^2.0.0", "stylelint-config-recommended": "^14.0.1", "stylelint-config-recommended-scss": "^14.1.0" }, @@ -3819,82 +3874,81 @@ } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/compose": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-8.6.0.tgz", - "integrity": "sha512-aurjKO5vgnKl3p4iRM/maMEB3z36e0FdAuJOvPTHKMUjd0OUk8n2M/n2mfRS68QxVHv8mmDm1IEg8zKobwIDmA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-8.10.0.tgz", + "integrity": "sha512-ilezxcwhnlCLBZHYjgRgVf4+QxIKb373WLrab47EVFck9GwdLlq1rBWcWCnXME6r1MJ1r8q30RTGSbA5shub6Q==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { "@types/mousetrap": "^1.6.8", - "@wordpress/deprecated": "^4.53.0", - "@wordpress/dom": "^4.53.0", - "@wordpress/element": "^8.5.0", - "@wordpress/is-shallow-equal": "^5.53.0", - "@wordpress/keycodes": "^4.53.0", - "@wordpress/priority-queue": "^3.53.0", - "@wordpress/private-apis": "^1.53.0", - "@wordpress/undo-manager": "^1.53.0", + "@wordpress/deprecated": "^4.57.0", + "@wordpress/dom": "^4.57.0", + "@wordpress/element": "^9.0.0", + "@wordpress/is-shallow-equal": "^5.57.0", + "@wordpress/keycodes": "^4.57.0", + "@wordpress/priority-queue": "^3.57.0", + "@wordpress/private-apis": "^2.0.0", + "@wordpress/undo-manager": "^1.57.0", "change-case": "^4.1.2", - "mousetrap": "^1.6.5", - "use-memo-one": "^1.1.1" + "mousetrap": "^1.6.5" }, "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" }, "peerDependencies": { "@types/react": "^18 || ^19", - "react": "^18 || ^19" + "@types/react-dom": "^18 || ^19", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/deprecated": { - "version": "4.53.0", - "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.53.0.tgz", - "integrity": "sha512-JaIWiGVoKEmsiWP4XwAZvrET44T34EtPG+xCJe9vPIs9O3DHPyB83VHV4WxVRxddaf1osTmR6iPLx0sBvVTjZw==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.57.0.tgz", + "integrity": "sha512-eRjYiN5wE8cmV+sehqGTHpb2VxwSqv/FMkcQ+0po4G3JK+Y1V0StqZRDhsgcwUocPRKuAtTO7E28fcdo615DFw==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/hooks": "^4.53.0" + "@wordpress/hooks": "^4.57.0" }, "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/dom": { - "version": "4.53.0", - "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.53.0.tgz", - "integrity": "sha512-cf+WkRxuXtPLmAWXYShsbitUIkPOjIgiScUJQztA4OSqy9vfSPvrOgb6pUqijPiIo9+fhBSoyryOFCOg7xABOQ==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.57.0.tgz", + "integrity": "sha512-KjnGUcxtdh8+Gvi0zUhh4iDRE8fsvVJkd798aS65HL6ysZYLuOVsdHwy5Qh6LT//tB3juSn1EMJ/TzdgtIOg6A==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/deprecated": "^4.53.0" + "@wordpress/deprecated": "^4.57.0" }, "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/element": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-8.5.0.tgz", - "integrity": "sha512-d6Ct+JETXRoGjfAxu5kSPjportHloZ3auAPLguQOLEoNBiZJp60fslYYd6LeVw7M7dUR3lArwcYVK0x0G06c9Q==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-9.0.0.tgz", + "integrity": "sha512-o4m80wTlAkqbdJ7F7vkmFzWx6B46fJ3eyG9dWu5BJ1adjL1tA1wv0xStTkFzKuU1iPHCRN0dhauS9G9rfdF3jw==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/deprecated": "^4.53.0", - "@wordpress/escape-html": "^3.53.0", + "@wordpress/escape-html": "^3.57.0", "change-case": "^4.1.2", - "is-plain-object": "^5.0.0" + "is-plain-object": "^5.1.0" }, "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" }, "peerDependencies": { "@types/react": "^18 || ^19", @@ -3912,36 +3966,34 @@ } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/escape-html": { - "version": "3.53.0", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.53.0.tgz", - "integrity": "sha512-6EE4hOsd0kUp4bO9voTsIeK0oa80FYMbxvJkjN9Vitnf0ZTRVB7Iz2IviaYorhGUDemlRy3kOPEZdP9DA/CoCA==", + "version": "3.57.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.57.0.tgz", + "integrity": "sha512-QT0x2X39blQjLHru11Dp00DAR0LLRMKMC6ATSWidliD3xQfKz+BjiPzTJd/LyhPLEE8C9li7ylKUlseIO2EYjQ==", "dev": true, "license": "GPL-2.0-or-later", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/hooks": { - "version": "4.53.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.53.0.tgz", - "integrity": "sha512-YU30rweJCTu5g7X8bc+w6QyoxJBYLEHH++Wwo/IbbROHUC/4I7e+ZtxnPWls8BTtMP4d28JJ74s+UoKpzd2J7w==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.57.0.tgz", + "integrity": "sha512-vRJxRhOj+a19RvdW6u0ZBmiQc/t/ueVGsIP3IbS9zuvfLJILtvMSWXU8Fe3m/W+mBK5ZEOfm72LYBhWGPse0DA==", "dev": true, "license": "GPL-2.0-or-later", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/i18n": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.26.0.tgz", - "integrity": "sha512-v/LzeB7T5YKIfBgmLM0jYG8zOuu81biZ90j8RY9pHIVu+jdRE+DSmt55S3FvtXRo5J2X9FoigFRzYPKXLbitWA==", + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.30.0.tgz", + "integrity": "sha512-Q8ivagc2aG+Qe5dTzmiohR+pDo4xDcD746fGI2dq+JIbTibYRnkFoN1VyGFZX/Q+g98I80ZJLJ5Vk3svSXb9ag==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.53.0", + "@wordpress/hooks": "^4.57.0", "gettext-parser": "^1.3.1", "tannin": "^1.2.0" }, @@ -3949,33 +4001,30 @@ "pot-to-php": "tools/pot-to-php.js" }, "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/is-shallow-equal": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.53.0.tgz", - "integrity": "sha512-NHhvEnqi82Vzon458Jz6nzmbAPWM1zy9k6rBXQTc+9PsRiWHWpqXqldKAeTFVrl0vspZf4PwmLlsxqP3ckCLWw==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.57.0.tgz", + "integrity": "sha512-QoIVWgnkKHAgrFB9hfpnmGvrAbutsa53DNl08i98guhtVVTSLzttlxgJxA3wpIepmR8FYiLNGB2Kfa49NdyIwg==", "dev": true, "license": "GPL-2.0-or-later", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/keycodes": { - "version": "4.53.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.53.0.tgz", - "integrity": "sha512-e2o0NjkyGpPUGz8sfRd1OeEHdeOk3UVlL1J8P4Y9/fgQQJumTjulbiwps6yheE5kRNqbrh2o424QLdMaODu9Kw==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.57.0.tgz", + "integrity": "sha512-IhPt1cAy00wxGECraOlWJMT+PwgLdh1CqS57RDsdhXMCs//AbIKFBMNxvEjPyq9aeU3looVJzcBR1zVhWkDErQ==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/i18n": "^6.26.0" + "@wordpress/i18n": "^6.30.0" }, "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" }, "peerDependencies": { "@types/react": "^18 || ^19" @@ -3987,41 +4036,45 @@ } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/priority-queue": { - "version": "3.53.0", - "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.53.0.tgz", - "integrity": "sha512-HQ5AatN1QWimFHQnhhhzrfgNoiVWTqwhUEv5326hhTnyUxvh5SOYxg46IBGow+zj/VJjIPyaxv1ftuqzVxgRMQ==", + "version": "3.57.0", + "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.57.0.tgz", + "integrity": "sha512-ZIfR49tJ8MKEARh/Ucy18QsVQXzeriFXcR0q+urvnCwaPwgpAY1bQo1a5oA0Mw5wO+uDvjGt7m5uZZeSxfHzxg==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { "requestidlecallback": "^0.3.0" }, "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" } }, "node_modules/@wordpress/stylelint-config/node_modules/@wordpress/theme": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@wordpress/theme/-/theme-1.2.0.tgz", - "integrity": "sha512-pILS4ytGGiASrpJ8sLp+pRbcBspFYeMTr5LdM4fWBv/EN2CIKhBLpX3qGlXwKzS+HzjrbXZwj60Bfbg/PxvQew==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@wordpress/theme/-/theme-2.3.0.tgz", + "integrity": "sha512-rPIBgKLq3WE0OiwNMBUGQcIpweLGYUjjt6IBn30J1i/Jvxpz8bE/SVMMZxN5R40orq3oswWXvjsuB1L61Bqvkg==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/compose": "^8.6.0", - "@wordpress/deprecated": "^4.53.0", - "@wordpress/element": "^8.5.0", - "@wordpress/private-apis": "^1.53.0", - "@wordpress/style-runtime": "^0.9.0", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/trace-mapping": "^0.3.31", + "@wordpress/compose": "^8.10.0", + "@wordpress/deprecated": "^4.57.0", + "@wordpress/element": "^9.0.0", + "@wordpress/private-apis": "^2.0.0", + "@wordpress/style-runtime": "^0.13.0", "colorjs.io": "^0.7.1", + "magic-string": "^0.30.21", "memize": "^2.1.1" }, "engines": { - "node": "^20.19.0 || >=22.13.0", - "npm": ">=10.2.3" + "node": "^20.19.0 || >=22.13.0" }, "peerDependencies": { "@types/react": "^18 || ^19", "esbuild": ">=0.27.2 <1.0.0", + "lightningcss": ">=1.33.0 <2.0.0", "postcss": "^8.0.0", "react": "^18 || ^19", "react-dom": "^18 || ^19", @@ -4035,9 +4088,18 @@ "esbuild": { "optional": true }, + "lightningcss": { + "optional": true + }, "postcss": { "optional": true }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, "stylelint": { "optional": true }, @@ -4047,9 +4109,9 @@ } }, "node_modules/@wordpress/stylelint-config/node_modules/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", "dev": true, "license": "MIT", "peer": true, @@ -4058,50 +4120,48 @@ } }, "node_modules/@wordpress/stylelint-config/node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "scheduler": "^0.27.0" + "scheduler": "^0.28.0" }, "peerDependencies": { - "react": "^19.2.8" + "react": "^19.3.1" } }, "node_modules/@wordpress/stylelint-config/node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@wordpress/undo-manager": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.53.0.tgz", - "integrity": "sha512-0MtlfeCa0uFWAGubP2rAJ1NoWQOWOuzW0kTwZgqfFFg81HEgNIBb8LU3eXMbMxERjdZYpCrqUL3MbVPl7ExiGw==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.57.0.tgz", + "integrity": "sha512-LGTnpgeuRKDiEMrfzfAxkivlhN6apCsIVx3v7lyRb3vcr1rM9j8JL34W/+sLDLaUafb4I1f/LEBFRB18F55/Gg==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/is-shallow-equal": "^5.53.0" + "@wordpress/is-shallow-equal": "^5.57.0" }, "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" } }, "node_modules/@wordpress/undo-manager/node_modules/@wordpress/is-shallow-equal": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.53.0.tgz", - "integrity": "sha512-NHhvEnqi82Vzon458Jz6nzmbAPWM1zy9k6rBXQTc+9PsRiWHWpqXqldKAeTFVrl0vspZf4PwmLlsxqP3ckCLWw==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.57.0.tgz", + "integrity": "sha512-QoIVWgnkKHAgrFB9hfpnmGvrAbutsa53DNl08i98guhtVVTSLzttlxgJxA3wpIepmR8FYiLNGB2Kfa49NdyIwg==", "dev": true, "license": "GPL-2.0-or-later", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=18.12.0" } }, "node_modules/@xtuc/ieee754": { @@ -4147,7 +4207,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -4456,7 +4518,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -4996,7 +5060,9 @@ "license": "MIT" }, "node_modules/colord": { - "version": "2.9.3", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", "dev": true, "license": "MIT" }, @@ -5152,9 +5218,9 @@ } }, "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -5240,9 +5306,9 @@ } }, "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -5549,14 +5615,6 @@ "node": ">=0.10.0" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, @@ -5698,15 +5756,13 @@ "license": "MIT" }, "node_modules/detect-libc": { - "version": "1.0.3", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/dir-glob": { @@ -5808,18 +5864,6 @@ "node": ">= 0.4" } }, - "node_modules/dunder-proto/node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.411", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz", @@ -6273,7 +6317,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6477,7 +6523,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.6", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true, "funding": [ { @@ -6706,18 +6754,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic/node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-proto": { "version": "1.0.1", "dev": true, @@ -7680,7 +7716,9 @@ } }, "node_modules/is-plain-object": { - "version": "5.0.0", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.1.0.tgz", + "integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==", "dev": true, "license": "MIT", "engines": { @@ -7900,8 +7938,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8033,7 +8083,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -8080,6 +8132,16 @@ "tslib": "^2.0.3" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/map-cache": { "version": "0.2.2", "dev": true, @@ -8196,7 +8258,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -8352,9 +8416,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -8899,9 +8963,9 @@ } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", "dev": true, "funding": [ { @@ -8919,9 +8983,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -8954,9 +9018,9 @@ } }, "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8985,9 +9049,9 @@ } }, "node_modules/postcss-calc/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9221,9 +9285,9 @@ } }, "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9261,9 +9325,9 @@ } }, "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9291,9 +9355,9 @@ } }, "node_modules/postcss-discard-comments/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9398,9 +9462,9 @@ } }, "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9438,9 +9502,9 @@ } }, "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9659,9 +9723,9 @@ } }, "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9744,9 +9808,9 @@ } }, "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9785,7 +9849,9 @@ } }, "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.0", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9811,7 +9877,9 @@ } }, "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.0", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9911,9 +9979,9 @@ } }, "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -10289,9 +10357,9 @@ } }, "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -10437,9 +10505,9 @@ } }, "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -10451,7 +10519,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz", + "integrity": "sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -10520,9 +10590,9 @@ } }, "node_modules/postcss-unique-selectors/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -11420,16 +11490,6 @@ "@img/sharp-win32-x64": "0.34.5" } }, - "node_modules/sharp/node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/shebang-regex": { "version": "3.0.0", "dev": true, @@ -11786,7 +11846,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -11794,12 +11856,14 @@ } }, "node_modules/source-map-resolve": { - "version": "0.5.3", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.0.tgz", + "integrity": "sha512-3HnSRlPHZMKs84V7TnCyhhTVJAWoghTh4lbLZbv1OMOt4X0hLx5c2zQF3qsWD11v2QK0OB4RKMYZnbaHsBJPeg==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", "dev": true, "license": "MIT", "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", + "atob": "^2.0.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", "urix": "^0.1.0" @@ -12028,9 +12092,9 @@ } }, "node_modules/stylehacks/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -12106,9 +12170,9 @@ } }, "node_modules/stylelint-config-recess-order": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-7.8.0.tgz", - "integrity": "sha512-ObYusdkXavm8ulgb5xvmU1l8wgCdveNYG12hOTUEdjS8bNhSRggl4or0mE0JaOnQz+Ve2xZemVpwqERCgJbntg==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-7.8.2.tgz", + "integrity": "sha512-IIRCTkrGiynshEapnjdc53Nq/EaoqdWr/5U0Kk42b9O026+CmgBu4BHQPraWdV98E/U/XsotAhWZXs5gS7/7Zw==", "dev": true, "license": "ISC", "peerDependencies": { @@ -12232,9 +12296,9 @@ "license": "CC0-1.0" }, "node_modules/stylelint-scss/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -12392,9 +12456,9 @@ } }, "node_modules/stylelint/node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -12797,16 +12861,18 @@ } }, "node_modules/svgo-loader/node_modules/svgo": { - "version": "2.8.0", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.4.tgz", + "integrity": "sha512-2GJ4h3rl13qYTdwllaK6QlL8tG+UrM8626V2Ylcd/yBUv2Y/EwLsYisVV6UDCRmlk+C74G8nMpxJCk0RWPdDCw==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", + "sax": "^1.5.0", "stable": "^0.1.8" }, "bin": { @@ -13021,14 +13087,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -13042,12 +13109,39 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } @@ -13512,6 +13606,17 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/watchpack": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", diff --git a/src/js/classes/Animation.js b/src/js/classes/Animation.js index 1bde520c..5cbc1d64 100644 --- a/src/js/classes/Animation.js +++ b/src/js/classes/Animation.js @@ -1,5 +1,5 @@ import AbstractDomElement from './AbstractDomElement' -import { ScrollObserver, SplittedText, ThrottledEvent } from 'oneloop.js' +import { SplittedText } from 'oneloop.js' import noop from '../utils/noop' // ---- @@ -7,8 +7,10 @@ import noop from '../utils/noop' // ---- const instances = [] const animationClass = 'js-animation' -let scrollObserver -let resize +// one IntersectionObserver per rootMargin, shared by all instances using it +const observers = new Map() +const lastScroll = { x: window.scrollX, y: window.scrollY } +let scrollUpdateScheduled = false // ---- // class Animation @@ -27,16 +29,10 @@ class Animation extends AbstractDomElement { return instance } - const that = this const el = this._element const s = this._settings - const start = getValue(el, s.start) - const end = getValue(el, s.end) const callbacksSharedData = {} - this._elementHeight = this._element.offsetHeight - this._windowHeight = window.innerHeight - this._onResize = onResize.bind(this) this._isVisible = false this._callbacksSharedData = callbacksSharedData @@ -48,44 +44,20 @@ class Animation extends AbstractDomElement { window.addEventListener('afterprint', onAfterPrint) } - // init scrollObserver and throttledEvent - if (!scrollObserver) { - scrollObserver = new ScrollObserver() - resize = new ThrottledEvent(window, 'resize') - } - - resize.add('resize', this._onResize) - // add animation class el.classList.add(s.animationClass) // intialize callback - s.onInit(el, scrollObserver.getScrollInfos(), callbacksSharedData) - - // add element to scrollObserver - scrollObserver.observe(el, { - onVisible: function (scrollInfos, percentRTW) { - const pStart = (this.distanceRTW.y * percentRTW.y) / that._windowHeight - const pEnd = (this.distanceRTW.y * percentRTW.y) / (that._windowHeight + that._elementHeight) - - if (!that._isVisible && pStart >= start && pEnd <= end) { - // show element - that._isVisible = true - s.onShow(el, scrollInfos, callbacksSharedData) - el.classList.add(s.visibleClass) - - // destroy if playOnce = true - if (s.playOnce) { - that.destroy(el, scrollInfos, callbacksSharedData) - } - } else if (that._isVisible && (pStart < start || (pEnd > end && s.hideOnReachEnd))) { - // hide element - that._isVisible = false - s.onHide(el, scrollInfos, callbacksSharedData) - el.classList.remove(s.visibleClass) - } - }, - }) + s.onInit(el, getScrollInfos(), callbacksSharedData) + + // without IntersectionObserver support, never leave content hidden + if (typeof IntersectionObserver === 'undefined') { + this._show(getScrollInfos()) + return + } + + this._rootMargin = getRootMargin(getValue(el, s.start), getValue(el, s.end), s.hideOnReachEnd) + getObserver(this._rootMargin).observe(el) } /** @@ -96,6 +68,63 @@ class Animation extends AbstractDomElement { return this._isVisible } + /** + * Show the element + * @param {object} scrollInfos + * @returns {undefined} + */ + _show(scrollInfos) { + const el = this._element + const s = this._settings + + this._isVisible = true + s.onShow(el, scrollInfos, this._callbacksSharedData) + el.classList.add(s.visibleClass) + + // destroy if playOnce = true + if (s.playOnce) { + this.destroy() + } + } + + /** + * Hide the element + * @param {object} scrollInfos + * @returns {undefined} + */ + _hide(scrollInfos) { + const el = this._element + const s = this._settings + + this._isVisible = false + s.onHide(el, scrollInfos, this._callbacksSharedData) + el.classList.remove(s.visibleClass) + } + + /** + * Handle an intersection change of the element + * @param {IntersectionObserverEntry} entry + * @param {object} scrollInfos + * @returns {undefined} + */ + _onIntersection(entry, scrollInfos) { + if (entry.isIntersecting) { + if (!this._isVisible) { + this._show(scrollInfos) + } + return + } + + if (!this._isVisible) { + return + } + + // element went back below the start line (scroll up), or above the end line when hideOnReachEnd is set + if (entry.boundingClientRect.top > 0 || this._settings.hideOnReachEnd) { + this._hide(scrollInfos) + } + } + /** * Destroy * @returns {undefined} @@ -105,7 +134,7 @@ class Animation extends AbstractDomElement { const s = this._settings const index = instances.indexOf(this) const callbacksSharedData = this._callbacksSharedData - let scrollInfos + const scrollInfos = getScrollInfos() if (index === -1) { return @@ -113,7 +142,6 @@ class Animation extends AbstractDomElement { super.destroy() - scrollInfos = scrollObserver.getScrollInfos() instances.splice(index, 1) if (s.showOnDestroy) { @@ -121,15 +149,13 @@ class Animation extends AbstractDomElement { el.classList.add(s.visibleClass) } - scrollObserver.unobserve(el) - resize.remove('resize', this._onResize) - - if (!scrollObserver.hasEntry()) { - scrollObserver = scrollObserver.destroy() - resize = resize.destroy() + if (this._rootMargin && observers.has(this._rootMargin)) { + observers.get(this._rootMargin).unobserve(el) } if (instances.length === 0) { + observers.forEach((observer) => observer.disconnect()) + observers.clear() window.removeEventListener('beforeprint', onBeforePrint) window.removeEventListener('afterprint', onAfterPrint) } @@ -167,6 +193,7 @@ Animation.defaults = { // start (relative to bottom of the screen), can be a float, a function (element) {} or an array of two values (range) [] start: 0.25, // end (relative to bottom of the screen), can be a float, a function (element) {} or an array of two values (range) [] + // only used when hideOnReachEnd is true end: 0.75, // if true, the instance will be destroyed after the element is visible playOnce: false, @@ -185,13 +212,20 @@ Animation.defaults = { // events // ---- /** - * onResize - * @param {Object} event + * Dispatch intersection entries to their instances + * @param {IntersectionObserverEntry[]} entries * @returns {undefined} */ -function onResize() { - this._elementHeight = this._element.offsetHeight - this._windowHeight = window.innerHeight +function onIntersection(entries) { + const scrollInfos = getScrollInfos() + + for (const entry of entries) { + const instance = entry.target.beapi && entry.target.beapi[Animation.nameSpace] + + if (instance) { + instance._onIntersection(entry, scrollInfos) + } + } } /** @@ -230,6 +264,70 @@ function getValue(element, value) { return rt } +/** + * Convert start / end settings (fractions of the viewport) to an IntersectionObserver rootMargin. + * Values are rounded to whole percents so instances share observers. + * @param {number} start + * @param {number} end + * @param {boolean} hideOnReachEnd + * @returns {string} + */ +function getRootMargin(start, end, hideOnReachEnd) { + const bottom = Math.round(clamp(start) * 100) + const top = hideOnReachEnd ? Math.round((1 - clamp(end)) * 100) : 0 + + return `-${top}% 0px -${bottom}% 0px` +} + +/** + * Clamp a value between 0 and 1 + * @param {number} value + * @returns {number} + */ +function clamp(value) { + return Math.min(1, Math.max(0, Number(value) || 0)) +} + +/** + * Get (or create) the shared observer for a rootMargin + * @param {string} rootMargin + * @returns {IntersectionObserver} + */ +function getObserver(rootMargin) { + if (!observers.has(rootMargin)) { + observers.set(rootMargin, new IntersectionObserver(onIntersection, { rootMargin })) + } + + return observers.get(rootMargin) +} + +/** + * Scroll infos passed to callbacks, same shape as oneloop.js ScrollObserver (direction.y = 1 when scrolling down). + * Computed on demand: no scroll listener is attached. + * lastScroll is committed on the next animation frame so every caller in the same turn + * (multiple IntersectionObservers, destroy) shares the same delta / direction. + * @returns {object} + */ +function getScrollInfos() { + const scroll = { x: window.scrollX, y: window.scrollY } + const delta = { x: scroll.x - lastScroll.x, y: scroll.y - lastScroll.y } + + if (!scrollUpdateScheduled) { + scrollUpdateScheduled = true + requestAnimationFrame(() => { + lastScroll.x = window.scrollX + lastScroll.y = window.scrollY + scrollUpdateScheduled = false + }) + } + + return { + scroll, + delta, + direction: { x: Math.sign(delta.x), y: Math.sign(delta.y) }, + } +} + // ---- // presets // ---- @@ -275,7 +373,9 @@ Animation.preset = { }) }, onDestroy: function (el, scrollInfos, data) { - data.splittedText.destroy() + if (data.splittedText) { + data.splittedText.destroy() + } }, }, } diff --git a/src/js/utils/deferInitialization.js b/src/js/utils/deferInitialization.js new file mode 100644 index 00000000..da33ff72 --- /dev/null +++ b/src/js/utils/deferInitialization.js @@ -0,0 +1,20 @@ +/** + * Run non-critical work when the main thread is idle, to keep it free for the first interactions (INP). + * Falls back to a short timeout when requestIdleCallback is not supported (Safari). + * + * @example + * import deferInitialization from '../utils/deferInitialization' + * deferInitialization(() => import('./classes/Slider')) + * + * @param {Function} callback + * @param {number} timeout Maximum delay (ms) before the callback is forced to run. + * @returns {undefined} + */ +export default function deferInitialization(callback, timeout = 2000) { + if (typeof window.requestIdleCallback === 'function') { + window.requestIdleCallback(callback, { timeout }) + return + } + + setTimeout(callback, 100) +} diff --git a/src/scss/02-tools/_f-assign-inputs.scss b/src/scss/02-tools/_f-assign-inputs.scss index 5deee2e2..2a8cdc7d 100644 --- a/src/scss/02-tools/_f-assign-inputs.scss +++ b/src/scss/02-tools/_f-assign-inputs.scss @@ -31,7 +31,7 @@ @each $input in $inputs { $input: string.unquote($input); - $input: if($pseudo, $input + ":" + $pseudo, $input); + $input: if(sass($pseudo): $input + ":" + $pseudo; else: $input); $list: list.append($list, $input, comma); } diff --git a/src/scss/03-base/_variables-css.scss b/src/scss/03-base/_variables-css.scss index babf0cc1..7661591d 100644 --- a/src/scss/03-base/_variables-css.scss +++ b/src/scss/03-base/_variables-css.scss @@ -1,3 +1,4 @@ +@use "sass:map"; @use "../01-abstract/variables" as *; @use "../02-tools/f-fluid-size" as *; @use "../02-tools/m-breakpoint" as *; @@ -7,7 +8,7 @@ /* * Breakpoints */ - --breakpoint-mobile-to-desktop-nav: #{map-get($breakpoints, mobile-to-desktop-nav)}; // Used by JavaScript + --breakpoint-mobile-to-desktop-nav: #{map.get($breakpoints, mobile-to-desktop-nav)}; // Used by JavaScript /* * Heading