Repository: symfony/ux
Pull request #2463 (2024-12-22 02:40:06)

[Stimulus] Fasten lazy loading + add debug `lazy:loading` and `lazy:loaded` 
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| Issues        | Fix #...
| License       | MIT

This PR..

1. improves the controller **lazy loading** by tuning algorithm (early exit, remove await, ...) 

2. introduces two **new debug events** in the console to ease DX with lazy controllers
    * `my-controller#lazy:loading` when the lazy controller is detected in the DOM
    * `my-controller#lazy:loaded` after the file has been downloaded and imported

I would really like some feedback / tests IRL (especially with Webpack i'm not used to)

github-actions[bot]: <h1>📊 Packages dist files size difference</h1>

Thanks for the PR! Here is the difference in size of the packages dist files between the base branch and the PR.
Please review the changes and make sure they are expected.

<table>
    <thead><tr><th>File</th><th>Before (Size / Gzip)</th><th>After (Size / Gzip)</th></tr></thead>
    <tbody><tr><td colspan="3"><a href="https://github.com/symfony/ux/tree/stimulus/lazy-loader/src/StimulusBundle/assets/dist"><b>StimulusBundle</b></a></td></tr><tr>
                <td><a href="https://github.com/symfony/ux/blob/stimulus/lazy-loader/src/StimulusBundle/assets/dist/loader.js"><code>loader.js</code></a></td>
            <td>
                    <code>3.2 kB</code>
                    / <code>910 B</code> 
                </td><td>
                    <code>3.8 kB</code><sup>+19% 📈</sup>
                    / <code>1.01 kB</code><sup>+14% 📈</sup>
                </td></tr></tbody>
</table>

<!-- Sticky Pull Request Comment -->
Kocal: > I would really like some feedback / tests IRL (especially with Webpack i'm not used to)

I will try to find some time to rebuild a Symfony + Encore app, but by reading your changes it looks fine to me (feel free to reping me if necessary)
smnandre: @MatTheCat  ? 👼 
Kocal: I'm testing
Kocal: Well... I've just realized, but to use Stimulus in a Symfony App with Webpack Encore, we use the https://github.com/symfony/stimulus-bridge ... which is not in this repository (and so not modified by this PR) 😅 
smnandre: Well.... let me tell you i have no intention of coding in the Bridge hahaha 

(i'll have a look but the lazy loading is done differently if i remember correctly)
MatTheCat: I’ve got nothing to test then? :thinking: 

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 12b8910b..1cfe2f9e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -90,21 +90,6 @@ Thanks to [Yarn Workspaces](https://yarnpkg.com/features/workspaces), you can al
   - `yarn run check`: run the formatter, linter, and sort imports for **all** packages, and fails if any modifications
   - `yarn run check --write`: run the formatter, linter, imports sorting for **all** packages, and write modifications
 
-### Working on documentation
-
-Symfony UX documentation is written in ReStructuredText (`.rst`) and is located in the `docs/` directory
-of each package.
-
-When contributing to the documentation, please make sure to follow the Symfony
-[documentation guidelines](https://symfony.com/doc/current/contributing/documentation/index.html).
-
-To verify your changes locally, you can use the `oskarstark/doctor-rst` Docker image. Run the following
-command from the root directory of the projet:
-
-```shell
-docker run --rm -it -e DOCS_DIR='/docs' -v ${PWD}:/docs  oskarstark/doctor-rst -vvv
-```
-
 ## Useful Git commands
 
 1. To keep your fork up-to-date with the upstream repository and `2.x` branch, you can run the following commands:
diff --git a/src/Autocomplete/CHANGELOG.md b/src/Autocomplete/CHANGELOG.md
index 1f8cea05..d7384fee 100644
--- a/src/Autocomplete/CHANGELOG.md
+++ b/src/Autocomplete/CHANGELOG.md
@@ -1,10 +1,5 @@
 # CHANGELOG
 
-## 2.23.0
-
--   Deprecate `ExtraLazyChoiceLoader` in favor of `Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader`
--   Reset TomSelect when updating url attribute #1505
-
 ## 2.22.0
 
 -   Take `labelField` TomSelect option into account #2382
diff --git a/src/Autocomplete/assets/dist/controller.d.ts b/src/Autocomplete/assets/dist/controller.d.ts
index d5cc6b3e..59938a72 100644
--- a/src/Autocomplete/assets/dist/controller.d.ts
+++ b/src/Autocomplete/assets/dist/controller.d.ts
@@ -40,7 +40,6 @@ export default class extends Controller {
     connect(): void;
     initializeTomSelect(): void;
     disconnect(): void;
-    urlValueChanged(): void;
     private getMaxOptions;
     get selectElement(): HTMLSelectElement | null;
     get formElement(): HTMLInputElement | HTMLSelectElement;
diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js
index e021770d..fb04294a 100644
--- a/src/Autocomplete/assets/dist/controller.js
+++ b/src/Autocomplete/assets/dist/controller.js
@@ -91,9 +91,6 @@ class default_1 extends Controller {
             }
         }
     }
-    urlValueChanged() {
-        this.resetTomSelect();
-    }
     getMaxOptions() {
         return this.selectElement ? this.selectElement.options.length : 50;
     }
@@ -134,6 +131,7 @@ class default_1 extends Controller {
             this.element.innerHTML = currentHtml;
             this.initializeTomSelect();
             this.tomSelect.setValue(currentValue);
+            this.startMutationObserver();
         }
     }
     changeTomSelectDisabledState(isDisabled) {
diff --git a/src/Autocomplete/assets/src/controller.ts b/src/Autocomplete/assets/src/controller.ts
index 6b68283b..38b324fc 100644
--- a/src/Autocomplete/assets/src/controller.ts
+++ b/src/Autocomplete/assets/src/controller.ts
@@ -128,10 +128,6 @@ export default class extends Controller {
         }
     }
 
-    urlValueChanged() {
-        this.resetTomSelect();
-    }
-
     #getCommonConfig(): Partial<TomSettings> {
         const plugins: TPluginHash = {};
 
@@ -395,6 +391,8 @@ export default class extends Controller {
             this.element.innerHTML = currentHtml;
             this.initializeTomSelect();
             this.tomSelect.setValue(currentValue);
+
+            this.startMutationObserver();
         }
     }
 
diff --git a/src/Autocomplete/doc/index.rst b/src/Autocomplete/doc/index.rst
index 33b635e3..d9dd23c4 100644
--- a/src/Autocomplete/doc/index.rst
+++ b/src/Autocomplete/doc/index.rst
@@ -250,7 +250,7 @@ to the options above, you can also pass:
 
 ``filter_query`` (default: ``null``)
     If you want to completely control the query made for the "search results",
-    use this option. This is incompatible with ``searchable_fields`` and ``max_results``::
+    use this option. This is incompatible with ``searchable_fields``::
 
         [
             'filter_query' => function(QueryBuilder $qb, string $query, EntityRepository $repository) {
diff --git a/src/Autocomplete/src/Form/BaseEntityAutocompleteType.php b/src/Autocomplete/src/Form/BaseEntityAutocompleteType.php
index 18804b77..f4e002d0 100644
--- a/src/Autocomplete/src/Form/BaseEntityAutocompleteType.php
+++ b/src/Autocomplete/src/Form/BaseEntityAutocompleteType.php
@@ -13,7 +13,6 @@ namespace Symfony\UX\Autocomplete\Form;
 
 use Symfony\Bridge\Doctrine\Form\Type\EntityType;
 use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader;
 use Symfony\Component\Form\Exception\RuntimeException;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\Options;
@@ -43,10 +42,6 @@ final class BaseEntityAutocompleteType extends AbstractType
                 return null;
             }
 
-            if (class_exists(LazyChoiceLoader::class)) {
-                return new LazyChoiceLoader($loader);
-            }
-
             return new ExtraLazyChoiceLoader($loader);
         };
 
diff --git a/src/Autocomplete/src/Form/ChoiceList/Loader/ExtraLazyChoiceLoader.php b/src/Autocomplete/src/Form/ChoiceList/Loader/ExtraLazyChoiceLoader.php
index 8f49ab4b..dfa3b0b4 100644
--- a/src/Autocomplete/src/Form/ChoiceList/Loader/ExtraLazyChoiceLoader.php
+++ b/src/Autocomplete/src/Form/ChoiceList/Loader/ExtraLazyChoiceLoader.php
@@ -17,8 +17,6 @@ use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
 
 /**
  * Loads choices on demand only.
- *
- * @deprecated since Autocomplete 2.23 and will be removed in 3.0, use `Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader` instead.
  */
 class ExtraLazyChoiceLoader implements ChoiceLoaderInterface
 {
diff --git a/src/Chartjs/CHANGELOG.md b/src/Chartjs/CHANGELOG.md
index 2e4ed856..0bbaa88d 100644
--- a/src/Chartjs/CHANGELOG.md
+++ b/src/Chartjs/CHANGELOG.md
@@ -1,9 +1,5 @@
 # CHANGELOG
 
-## 2.23.0
-
--   Listen to Stimulus `disconnect` event to destroy the chart #1944
-
 ## 2.18.0
 
 -   Replace `chartjs/auto` import with explicit `Chart.register()` call #1263
diff --git a/src/Chartjs/assets/dist/controller.d.ts b/src/Chartjs/assets/dist/controller.d.ts
index bc05560d..4f1287e9 100644
--- a/src/Chartjs/assets/dist/controller.d.ts
+++ b/src/Chartjs/assets/dist/controller.d.ts
@@ -6,7 +6,6 @@ export default class extends Controller {
     };
     private chart;
     connect(): void;
-    disconnect(): void;
     viewValueChanged(): void;
     private dispatchEvent;
 }
diff --git a/src/Chartjs/assets/dist/controller.js b/src/Chartjs/assets/dist/controller.js
index 7caa0cb1..9432a885 100644
--- a/src/Chartjs/assets/dist/controller.js
+++ b/src/Chartjs/assets/dist/controller.js
@@ -35,13 +35,6 @@ class default_1 extends Controller {
         this.chart = new Chart(canvasContext, payload);
         this.dispatchEvent('connect', { chart: this.chart });
     }
-    disconnect() {
-        this.dispatchEvent('disconnect', { chart: this.chart });
-        if (this.chart) {
-            this.chart.destroy();
-            this.chart = null;
-        }
-    }
     viewValueChanged() {
         if (this.chart) {
             const viewValue = { data: this.viewValue.data, options: this.viewValue.options };
diff --git a/src/Chartjs/assets/src/controller.ts b/src/Chartjs/assets/src/controller.ts
index 78b7daaa..5888eb3c 100644
--- a/src/Chartjs/assets/src/controller.ts
+++ b/src/Chartjs/assets/src/controller.ts
@@ -57,15 +57,6 @@ export default class extends Controller {
         this.dispatchEvent('connect', { chart: this.chart });
     }
 
-    disconnect() {
-        this.dispatchEvent('disconnect', { chart: this.chart });
-
-        if (this.chart) {
-            this.chart.destroy();
-            this.chart = null;
-        }
-    }
-
     /**
      * If the underlying data or options change, let's update the chart!
      */
diff --git a/src/Icons/src/Iconify.php b/src/Icons/src/Iconify.php
index fcbebe81..564b87f2 100644
--- a/src/Icons/src/Iconify.php
+++ b/src/Icons/src/Iconify.php
@@ -126,12 +126,7 @@ final class Iconify
         $data = $response->toArray();
 
         $icons = [];
-        foreach ($names as $iconName) {
-            $iconData = $data['icons'][$data['aliases'][$iconName]['parent'] ?? $iconName] ?? null;
-            if (!$iconData) {
-                continue;
-            }
-
+        foreach ($data['icons'] as $iconName => $iconData) {
             $height = $iconData['height'] ?? $data['height'] ??= $this->sets()[$prefix]['height'] ?? null;
             $width = $iconData['width'] ?? $data['width'] ??= $this->sets()[$prefix]['width'] ?? null;
 
diff --git a/src/LazyImage/doc/index.rst b/src/LazyImage/doc/index.rst
index 2454ab94..b8369e0f 100644
--- a/src/LazyImage/doc/index.rst
+++ b/src/LazyImage/doc/index.rst
@@ -1,12 +1,6 @@
 Symfony UX LazyImage
 ====================
 
-.. warning::
-
-    **This package is no longer recommended.** Instead, use the
-    `modern techniques to improve image loading performance`_ natively supported
-    by all major browsers.
-
 Symfony UX LazyImage is a Symfony bundle providing utilities to improve
 image loading performance. It is part of `the Symfony UX initiative`_.
 
@@ -251,7 +245,6 @@ This bundle aims at following the same Backward Compatibility promise as
 the Symfony framework:
 https://symfony.com/doc/current/contributing/code/bc.html
 
-.. _`modern techniques to improve image loading performance`: https://web.dev/learn/images/performance-issues
 .. _`the Symfony UX initiative`: https://ux.symfony.com/
 .. _`BlurHash implementation`: https://blurha.sh
 .. _`StimulusBundle`: https://symfony.com/bundles/StimulusBundle/current/index.html
diff --git a/src/LiveComponent/CHANGELOG.md b/src/LiveComponent/CHANGELOG.md
index a43e9518..5674d75e 100644
--- a/src/LiveComponent/CHANGELOG.md
+++ b/src/LiveComponent/CHANGELOG.md
@@ -1,9 +1,5 @@
 # CHANGELOG
 
-## 2.23.0
-
--   Allow configuring the secret used to compute fingerprints and checksums.
-
 ## 2.22.0
 
 -   Remove CSRF tokens - rely on same-origin/CORS instead
diff --git a/src/LiveComponent/doc/index.rst b/src/LiveComponent/doc/index.rst
index 4c7b5fc2..ad3cc216 100644
--- a/src/LiveComponent/doc/index.rst
+++ b/src/LiveComponent/doc/index.rst
@@ -176,15 +176,6 @@ fresh copy of our component. That HTML will replace the current HTML. In
 other words, you just generated a new random number! That's cool, but
 let's keep going because… things get cooler.
 
-.. tip::
-
-    If you use the `Symfony MakerBundle`_, you can easily create a new component
-    with the ``make:twig-component`` command:
-
-    .. code-block:: terminal
-
-        $ php bin/console make:twig-component --live EditPost
-
 .. tip::
 
     Need to do some extra data initialization on your component? Create
@@ -243,7 +234,7 @@ LiveProps must be a value that can be sent to JavaScript. Supported values
 are scalars (int, float, string, bool, null), arrays (of scalar values), enums,
 DateTime objects, Doctrine entity objects, DTOs, or array of DTOs.
 
-See :ref:`hydration <hydration>` for handling more complex data.
+See :ref:`hydration` for handling more complex data.
 
 Data Binding
 ------------
@@ -1464,7 +1455,9 @@ or omit it entirely to let the ``initialFormData`` property default to ``null``:
     {# templates/post/new.html.twig #}
     {# ... #}
 
-    {{ component('PostForm') }}
+    {{ component('PostForm', {
+        form: form
+    }) }}
 
 Submitting the Form via a LiveAction
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
index dc04bfb1..28979e13 100644
--- a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
+++ b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
@@ -12,17 +12,12 @@
 namespace Symfony\UX\LiveComponent\DependencyInjection;
 
 use Symfony\Component\AssetMapper\AssetMapperInterface;
-use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
-use Symfony\Component\Config\Definition\Builder\TreeBuilder;
-use Symfony\Component\Config\Definition\ConfigurationInterface;
-use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
 use Symfony\Component\DependencyInjection\ChildDefinition;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\DependencyInjection\Definition;
 use Symfony\Component\DependencyInjection\Extension\Extension;
 use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
-use Symfony\Component\DependencyInjection\Parameter;
 use Symfony\Component\DependencyInjection\Reference;
 use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
 use Symfony\UX\LiveComponent\ComponentValidator;
@@ -55,12 +50,14 @@ use Symfony\UX\LiveComponent\Util\TwigAttributeHelperFactory;
 use Symfony\UX\TwigComponent\ComponentFactory;
 use Symfony\UX\TwigComponent\ComponentRenderer;
 
+use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_iterator;
+
 /**
  * @author Kevin Bond <kevinbond@gmail.com>
  *
  * @internal
  */
-final class LiveComponentExtension extends Extension implements PrependExtensionInterface, ConfigurationInterface
+final class LiveComponentExtension extends Extension implements PrependExtensionInterface
 {
     public const TEMPLATES_MAP_FILENAME = 'live_components_twig_templates.map';
 
@@ -96,19 +93,16 @@ final class LiveComponentExtension extends Extension implements PrependExtension
             }
         );
 
-        $configuration = $this->getConfiguration($configs, $container);
-        $config = $this->processConfiguration($configuration, $configs);
-
         $container->registerForAutoconfiguration(HydrationExtensionInterface::class)
             ->addTag(LiveComponentBundle::HYDRATION_EXTENSION_TAG);
 
         $container->register('ux.live_component.component_hydrator', LiveComponentHydrator::class)
             ->setArguments([
-                new TaggedIteratorArgument(LiveComponentBundle::HYDRATION_EXTENSION_TAG),
+                tagged_iterator(LiveComponentBundle::HYDRATION_EXTENSION_TAG),
                 new Reference('property_accessor'),
                 new Reference('ux.live_component.metadata_factory'),
                 new Reference('serializer', ContainerInterface::NULL_ON_INVALID_REFERENCE),
-                $config['secret'], // defaults to '%kernel.secret%'
+                '%kernel.secret%',
             ])
         ;
 
@@ -242,7 +236,7 @@ final class LiveComponentExtension extends Extension implements PrependExtension
 
         $container->register('ux.live_component.deterministic_id_calculator', DeterministicTwigIdCalculator::class);
         $container->register('ux.live_component.fingerprint_calculator', FingerprintCalculator::class)
-            ->setArguments([$config['secret']]); // default to %kernel.secret%
+            ->setArguments(['%kernel.secret%']);
 
         $container->setAlias(ComponentValidatorInterface::class, ComponentValidator::class);
 
@@ -259,40 +253,11 @@ final class LiveComponentExtension extends Extension implements PrependExtension
             ->setArguments([
                 new Reference('twig.template_iterator'),
                 self::TEMPLATES_MAP_FILENAME,
-                new Parameter('container.build_hash'),
+                '%kernel.secret%',
             ])
             ->addTag('kernel.cache_warmer');
     }
 
-    public function getConfigTreeBuilder(): TreeBuilder
-    {
-        $treeBuilder = new TreeBuilder('live_component');
-        $rootNode = $treeBuilder->getRootNode();
-        \assert($rootNode instanceof ArrayNodeDefinition);
-
-        $rootNode
-            ->addDefaultsIfNotSet()
-            ->children()
-                ->scalarNode('secret')
-                    ->info('The secret used to compute fingerprints and checksums')
-                    ->beforeNormalization()
-                        ->ifString()
-                        ->then(trim(...))
-                    ->end()
-                    ->cannotBeEmpty()
-                    ->defaultValue('%kernel.secret%')
-                ->end()
-            ->end()
-        ;
-
-        return $treeBuilder;
-    }
-
-    public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface
-    {
-        return $this;
-    }
-
     private function isAssetMapperAvailable(ContainerBuilder $container): bool
     {
         if (!interface_exists(AssetMapperInterface::class)) {
diff --git a/src/LiveComponent/src/LiveComponentHydrator.php b/src/LiveComponent/src/LiveComponentHydrator.php
index 95f4bf91..a5b23ce0 100644
--- a/src/LiveComponent/src/LiveComponentHydrator.php
+++ b/src/LiveComponent/src/LiveComponentHydrator.php
@@ -50,11 +50,8 @@ final class LiveComponentHydrator
         private PropertyAccessorInterface $propertyAccessor,
         private LiveComponentMetadataFactory $liveComponentMetadataFactory,
         private NormalizerInterface|DenormalizerInterface|null $serializer,
-        #[\SensitiveParameter] private string $secret,
+        private string $secret,
     ) {
-        if (!$secret) {
-            throw new \InvalidArgumentException('A non-empty secret is required.');
-        }
     }
 
     public function dehydrate(object $component, ComponentAttributes $attributes, LiveComponentMetadata $componentMetadata): DehydratedProps
diff --git a/src/LiveComponent/src/Util/FingerprintCalculator.php b/src/LiveComponent/src/Util/FingerprintCalculator.php
index 9134e1ca..0f3da0e9 100644
--- a/src/LiveComponent/src/Util/FingerprintCalculator.php
+++ b/src/LiveComponent/src/Util/FingerprintCalculator.php
@@ -24,14 +24,11 @@ use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadata;
  *
  * @internal
  */
-final class FingerprintCalculator
+class FingerprintCalculator
 {
     public function __construct(
-        #[\SensitiveParameter] private string $secret,
+        private string $secret,
     ) {
-        if (!$secret) {
-            throw new \InvalidArgumentException('A non-empty secret is required.');
-        }
     }
 
     public function calculateFingerprint(array $inputProps, LiveComponentMetadata $liveMetadata): string
diff --git a/src/StimulusBundle/assets/dist/loader.js b/src/StimulusBundle/assets/dist/loader.js
index 1d1ce80d..3f70fed3 100644
--- a/src/StimulusBundle/assets/dist/loader.js
+++ b/src/StimulusBundle/assets/dist/loader.js
@@ -25,30 +25,50 @@ class StimulusLazyControllerHandler {
         this.lazyLoadNewControllers(document.documentElement);
     }
     lazyLoadExistingControllers(element) {
-        this.queryControllerNamesWithin(element).forEach((controllerName) => this.loadLazyController(controllerName));
+        Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
+            .flatMap(extractControllerNamesFrom)
+            .forEach((controllerName) => this.loadLazyController(controllerName));
     }
-    async loadLazyController(name) {
-        if (canRegisterController(name, this.application)) {
-            if (this.lazyControllers[name] === undefined) {
-                return;
-            }
-            const controllerModule = await this.lazyControllers[name]();
-            registerController(name, controllerModule.default, this.application);
+    loadLazyController(name) {
+        if (!this.lazyControllers[name]) {
+            return;
+        }
+        const controllerLoader = this.lazyControllers[name];
+        delete this.lazyControllers[name];
+        if (!canRegisterController(name, this.application)) {
+            return;
         }
+        this.application.logDebugActivity(name, 'lazy:loading');
+        controllerLoader()
+            .then((controllerModule) => {
+            this.application.logDebugActivity(name, 'lazy:loaded');
+            registerController(name, controllerModule.default, this.application);
+        })
+            .catch((error) => {
+            console.error(`Error loading controller "${name}":`, error);
+        });
     }
     lazyLoadNewControllers(element) {
+        if (Object.keys(this.lazyControllers).length === 0) {
+            return;
+        }
         new MutationObserver((mutationsList) => {
-            for (const { attributeName, target, type } of mutationsList) {
-                switch (type) {
-                    case 'attributes': {
-                        if (attributeName === controllerAttribute &&
-                            target.getAttribute(controllerAttribute)) {
-                            extractControllerNamesFrom(target).forEach((controllerName) => this.loadLazyController(controllerName));
+            for (const mutation of mutationsList) {
+                switch (mutation.type) {
+                    case 'childList': {
+                        for (const node of mutation.addedNodes) {
+                            if (node instanceof Element) {
+                                extractControllerNamesFrom(node).forEach((controllerName) => {
+                                    this.loadLazyController(controllerName);
+                                });
+                            }
                         }
                         break;
                     }
-                    case 'childList': {
-                        this.lazyLoadExistingControllers(target);
+                    case 'attributes': {
+                        if (mutation.attributeName === controllerAttribute) {
+                            extractControllerNamesFrom(mutation.target).forEach((controllerName) => this.loadLazyController(controllerName));
+                        }
                     }
                 }
             }
@@ -58,9 +78,6 @@ class StimulusLazyControllerHandler {
             childList: true,
         });
     }
-    queryControllerNamesWithin(element) {
-        return Array.from(element.querySelectorAll(`[${controllerAttribute}]`)).flatMap(extractControllerNamesFrom);
-    }
 }
 function registerController(name, controller, application) {
     if (canRegisterController(name, application)) {
diff --git a/src/StimulusBundle/assets/src/loader.ts b/src/StimulusBundle/assets/src/loader.ts
index 5b5009ae..393ded69 100644
--- a/src/StimulusBundle/assets/src/loader.ts
+++ b/src/StimulusBundle/assets/src/loader.ts
@@ -64,40 +64,61 @@ class StimulusLazyControllerHandler {
     }
 
     private lazyLoadExistingControllers(element: Element) {
-        this.queryControllerNamesWithin(element).forEach((controllerName) => this.loadLazyController(controllerName));
+        Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
+            .flatMap(extractControllerNamesFrom)
+            .forEach((controllerName) => this.loadLazyController(controllerName));
     }
 
-    private async loadLazyController(name: string) {
-        if (canRegisterController(name, this.application)) {
-            if (this.lazyControllers[name] === undefined) {
-                return;
-            }
+    private loadLazyController(name: string) {
+        if (!this.lazyControllers[name]) {
+            return;
+        }
 
-            const controllerModule = await this.lazyControllers[name]();
+        // Delete the loader to avoid loading it twice
+        const controllerLoader = this.lazyControllers[name];
+        delete this.lazyControllers[name];
 
-            registerController(name, controllerModule.default, this.application);
+        if (!canRegisterController(name, this.application)) {
+            return;
         }
+
+        this.application.logDebugActivity(name, 'lazy:loading');
+
+        controllerLoader()
+            .then((controllerModule) => {
+                this.application.logDebugActivity(name, 'lazy:loaded');
+                registerController(name, controllerModule.default, this.application);
+            })
+            .catch((error) => {
+                console.error(`Error loading controller "${name}":`, error);
+            });
     }
 
     private lazyLoadNewControllers(element: Element) {
+        if (Object.keys(this.lazyControllers).length === 0) {
+            return;
+        }
         new MutationObserver((mutationsList) => {
-            for (const { attributeName, target, type } of mutationsList) {
-                switch (type) {
-                    case 'attributes': {
-                        if (
-                            attributeName === controllerAttribute &&
-                            (target as Element).getAttribute(controllerAttribute)
-                        ) {
-                            extractControllerNamesFrom(target as Element).forEach((controllerName) =>
-                                this.loadLazyController(controllerName)
-                            );
+            for (const mutation of mutationsList) {
+                switch (mutation.type) {
+                    case 'childList': {
+                        // @ts-ignore
+                        for (const node of mutation.addedNodes) {
+                            if (node instanceof Element) {
+                                extractControllerNamesFrom(node).forEach((controllerName) => {
+                                    this.loadLazyController(controllerName);
+                                });
+                            }
                         }
-
                         break;
                     }
 
-                    case 'childList': {
-                        this.lazyLoadExistingControllers(target as Element);
+                    case 'attributes': {
+                        if (mutation.attributeName === controllerAttribute) {
+                            extractControllerNamesFrom(mutation.target as Element).forEach((controllerName) =>
+                                this.loadLazyController(controllerName)
+                            );
+                        }
                     }
                 }
             }
@@ -107,10 +128,6 @@ class StimulusLazyControllerHandler {
             childList: true,
         });
     }
-
-    private queryControllerNamesWithin(element: Element): string[] {
-        return Array.from(element.querySelectorAll(`[${controllerAttribute}]`)).flatMap(extractControllerNamesFrom);
-    }
 }
 
 function registerController(name: string, controller: ControllerConstructor, application: Application) {
diff --git a/src/StimulusBundle/doc/index.rst b/src/StimulusBundle/doc/index.rst
index 286f3871..62521ff2 100644
--- a/src/StimulusBundle/doc/index.rst
+++ b/src/StimulusBundle/doc/index.rst
@@ -574,7 +574,7 @@ it will normalize it:
 .. _`Typed`: https://github.com/mattboldt/typed.js/
 .. _`Vue`: https://vuejs.org/
 .. _`stimulus-use`: https://stimulus-use.github.io/stimulus-use
-.. _`stimulus-components`: https://www.stimulus-components.com/
+.. _`stimulus-components`: https://stimulus-components.netlify.app/
 .. _`TypeScript`: https://www.typescriptlang.org/
 .. _`sensiolabs/typescript-bundle`: https://github.com/sensiolabs/AssetMapperTypeScriptBundle
 .. _`Stimulus plugin`: https://plugins.jetbrains.com/plugin/24562-stimulus
diff --git a/src/TwigComponent/doc/index.rst b/src/TwigComponent/doc/index.rst
index f06882e2..27d04c30 100644
--- a/src/TwigComponent/doc/index.rst
+++ b/src/TwigComponent/doc/index.rst
@@ -39,9 +39,9 @@ Done! Now render it wherever you want:
 Enjoy your new component!
 
 .. image:: images/alert-example.png
-    :alt: Example of the Alert Component
+   :alt: Example of the Alert Component
 
-    Example of the Alert Component
+   Example of the Alert Component
 
 This brings the familiar "component" system from client-side frameworks
 into Symfony. Combine this with `Live Components`_, to create
@@ -136,9 +136,14 @@ Take a moment to fist pump - then come back!
 Naming Your Component
 ~~~~~~~~~~~~~~~~~~~~~
 
+.. versionadded:: 2.8
+
+    Before 2.8, passing a name to ``AsTwigComponent`` was required. Now, the
+    name is optional and defaults to the class name.
+
 To give your component a name, TwigComponent looks at the namespace(s)
 configured in :ref:`twig_component.yaml <default_config>` and finds the
-first match. If you have the recommended ``App\Twig\Components\``, then:
+first match. If your have the recommended ``App\Twig\Components\``, then:
 
 ========================================  ===================
 Component Class                            Component Name
@@ -377,7 +382,7 @@ use the full path of the template where the macro is defined:
     <twig:Alert>
         {# ❌ this won't work #}
         {% from _self import message_formatter %}
-
+        
         {# ✅ this works as expected #}
         {% from 'path/of/this/template.html.twig' import message_formatter %}
 
@@ -390,7 +395,7 @@ Fetching Services
 Let's create a more complex example: a "featured products" component.
 You *could* choose to pass an array of Product objects to the component
 and set those on a ``$products`` property. But instead, let's let the
-*component* do the work of executing the query.
+*component* to do the work of executing the query.
 
 How? Components are *services*, which means autowiring works like
 normal. This example assumes you have a ``Product`` Doctrine entity and
@@ -405,8 +410,11 @@ normal. This example assumes you have a ``Product`` Doctrine entity and
     #[AsTwigComponent]
     class FeaturedProducts
     {
-        public function __construct(private ProductRepository $productRepository)
+        private ProductRepository $productRepository;
+
+        public function __construct(ProductRepository $productRepository)
         {
+            $this->productRepository = $productRepository;
         }
 
         public function getProducts(): array
@@ -466,7 +474,7 @@ called ``mount()``::
         public string $message;
         public string $type = 'success';
 
-        public function mount(bool $isSuccess = true): void
+        public function mount(bool $isSuccess = true)
         {
             $this->type = $isSuccess ? 'success' : 'danger';
         }
@@ -553,6 +561,10 @@ The data returned from ``preMount()`` will be used as the props for mounting.
 PostMount Hook
 ~~~~~~~~~~~~~~
 
+.. versionadded:: 2.1
+
+    The ``PostMount`` hook was added in TwigComponents 2.1.
+
 After a component is instantiated and its data mounted, you can run extra
 code via the ``PostMount`` hook::
 
@@ -740,11 +752,11 @@ There is also a non-HTML syntax that can be used:
 Context / Variables Inside of Blocks
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-The content inside of the ``<twig:Component>`` should be viewed as living in its own,
+The content inside of the ``<twig:{Component}>`` should be viewed as living in its own,
 independent template, which extends the component's template. This has a few interesting
 consequences.
 
-First, inside of ``<twig:Component>``, the ``this`` variable represents
+First, inside of ``<twig:{Component}>``, the ``this`` variable represents
 the component you're *now* rendering *and* you have access to all of *that*
 component's variables:
 
@@ -802,12 +814,10 @@ When overriding the ``alert_message`` block, you have access to the ``message``
 
 .. versionadded:: 2.13
 
-    The ability to refer to the scope of higher components via the ``outerScope``
-    variable was added in 2.13.
+    The ability to refer to the scope of higher components via the ``outerScope`` variable was added in 2.13.
 
-As mentioned before, variables from lower components are merged with those from
-upper components. When you need access to some properties or functions from higher
-components, that can be done via the ``outerScope...`` variable:
+As mentioned before, variables from lower components are merged with those from upper components. When you need
+access to some properties or functions from higher components, that can be done via the ``outerScope...`` variable:
 
 .. code-block:: twig
 
@@ -844,6 +854,10 @@ Remember though that the ``outerScope`` reference only starts once you're INSIDE
 Inheritance & Forwarding "Outer Blocks"
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
+.. versionadded:: 2.10
+
+    The ``outerBlocks`` variable was added in 2.10.
+
 The content inside a ``<twig:`` component tag should be viewed as living in
 its own, independent template, which *extends* the component's template. This means that
 any blocks that live in the "outer" template are not available. However, you
@@ -958,13 +972,12 @@ To add a custom `Stimulus controller`_ to your root component element:
 
     <div {{ attributes.defaults(stimulus_controller('my-controller', {someValue: 'foo'})) }}>
 
-.. note::
+.. versionadded:: 2.9
 
-    The ``stimulus_controller()`` function requires ``symfony/stimulus-bundle``.
-
-    .. code-block:: terminal
-
-        $ composer require symfony/stimulus-bundle
+    The ability to use ``stimulus_controller()`` with ``attributes.defaults()``
+    was added in TwigComponents 2.9 and requires ``symfony/stimulus-bundle``.
+    Previously, ``stimulus_controller()`` was passed to an ``attributes.add()``
+    method.
 
 .. note::
 
@@ -1173,16 +1186,21 @@ The nesting is recursive so you could potentially do something like this:
 Component with Complex Variants (CVA)
 -------------------------------------
 
+.. versionadded:: 2.16
+
+    The ``cva`` function was added in TwigComponents 2.16.
+
 .. deprecated:: 2.20
 
-    The ``cva`` function was deprecated in TwigComponents 2.20, and will be 
-    removed in 3.0. The function is now provided by the ``twig/html-extra:^3.12``
-    package under the name `html_cva`_.
+    The ``cva`` function was deprecated in TwigComponents 2.20, and will be removed in 3.0.
+    The function is now provided by the ``twig/html-extra:^3.12`` package under the name `html_cva`_.
 
-`CVA (Class Variant Authority)`_ originates from the JavaScript ecosystem. It
-enables reusable, customizable components by managing variants (e.g., color, size).
-The ``cva()`` Twig function defines ``base`` classes (always applied) and variant-specific
-classes:
+`CVA (Class Variant Authority)`_ is a concept from the JavaScript world and used
+by the well-known `shadcn/ui`_.
+CVA allows you to display a component with different variants (color, size, etc.),
+to create highly reusable and customizable components. This is powered by a ``cva()`` Twig
+function where you define ``base`` classes that should always be present and then different
+``variants`` and the corresponding classes:
 
 .. code-block:: html+twig
 
@@ -1213,33 +1231,52 @@ Then use the ``color`` and ``size`` variants to select the classes needed:
 
 .. code-block:: html+twig
 
+    {# index.html.twig #}
+    <twig:Alert color="red" size="lg">
+        <div>My content</div>
+    </twig:Alert>
+    // class="alert bg-red text-lg"
+
     <twig:Alert color="green" size="sm">
-        ...
+        <div>My content</div>
     </twig:Alert>
-    
-    {# will render as: #}
-    
-     <div class="alert bg-green text-sm">
-        ...
-    </div>
-    
+    // class="alert bg-green text-sm"
+
+    <twig:Alert color="red" class="flex items-center justify-center">
+        <div>My content</div>
+    </twig:Alert>
+    // class="alert bg-red text-md flex items-center justify-center"
+
 CVA and Tailwind CSS
 ~~~~~~~~~~~~~~~~~~~~
 
-CVA integrates seamlessly with Tailwind CSS, though class conflicts may occur.
-Use the ``tailwind_merge()`` function from `tales-from-a-dev/twig-tailwind-extra`_ 
-to resolve conflicts:
+CVA work perfectly with Tailwind CSS. The only drawback is that you can have class conflicts.
+To "merge" conflicting classes together and keep only the ones you need, use the
+``tailwind_merge()`` method from `tales-from-a-dev/twig-tailwind-extra`_
+with the ``cva()`` function:
+
+.. code-block:: terminal
+
+    $ composer require tales-from-a-dev/twig-tailwind-extra
 
 .. code-block:: html+twig
 
+    {# templates/components/Alert.html.twig #}
+    {% props color = 'blue', size = 'md' %}
+
+   {% set alert = cva({
+       // ...
+    }) %}
+
     <div class="{{ alert.apply({color, size}, attributes.render('class'))|tailwind_merge }}">
-        {% block content %}{% endblock %}
+         {% block content %}{% endblock %}
     </div>
 
 Compound Variants
 ~~~~~~~~~~~~~~~~~
 
-Define compound variants for conditions involving multiple variants:
+You can define compound variants. A compound variant is a variant that applies
+when multiple other variant conditions are met.
 
 .. code-block:: html+twig
 
@@ -1249,12 +1286,21 @@ Define compound variants for conditions involving multiple variants:
     {% set alert = cva({
         base: 'alert',
         variants: {
-           color: { red: 'bg-red' },
-           size: { lg: 'text-lg' }
+            color: {
+                blue: 'bg-blue',
+                red: 'bg-red',
+                green: 'bg-green',
+            },
+            size: {
+                sm: 'text-sm',
+                md: 'text-md',
+                lg: 'text-lg',
+            }
         },
         compoundVariants: [{
+            // if color = red AND size = (md or lg), add the `font-bold` class
             color: ['red'],
-            size: ['lg'],
+            size: ['md', 'lg'],
             class: 'font-bold'
         }]
     }) %}
@@ -1266,14 +1312,19 @@ Define compound variants for conditions involving multiple variants:
     {# index.html.twig #}
 
     <twig:Alert color="red" size="lg">
-        ...
+        <div>My content</div>
     </twig:Alert>
-    
-    {# will render as: #}
-    
-    <div class="alert bg-red text-lg font-bold">
-        ...
-    </div>
+    // class="alert bg-red text-lg font-bold"
+
+    <twig:Alert color="green" size="sm">
+        <div>My content</div>
+    </twig:Alert>
+    // class="alert bg-green text-sm"
+
+    <twig:Alert color="red" size="md">
+        <div>My content</div>
+    </twig:Alert>
+    // class="alert bg-green text-lg font-bold"
 
 Default Variants
 ~~~~~~~~~~~~~~~~
@@ -1283,33 +1334,42 @@ If no variants match, you can define a default set of classes to apply:
 .. code-block:: html+twig
 
     {# templates/components/Alert.html.twig #}
+    {% props color = 'blue', size = 'md' %}
+
     {% set alert = cva({
         base: 'alert',
         variants: {
             color: {
-                red: 'bg-red'
+                blue: 'bg-blue',
+                red: 'bg-red',
+                green: 'bg-green',
+            },
+            size: {
+                sm: 'text-sm',
+                md: 'text-md',
+                lg: 'text-lg',
             },
             rounded: {
                 sm: 'rounded-sm',
-                md: 'rounded-md'
+                md: 'rounded-md',
+                lg: 'rounded-lg',
             }
         },
         defaultVariants: {
-            rounded: 'md'
+            rounded: 'md',
         }
     }) %}
 
+    <div class="{{ alert.apply({color, size}) }}">
+         {% block content %}{% endblock %}
+    </div>
+
     {# index.html.twig #}
 
-    <twig:Alert color="red">
-        ...
+    <twig:Alert color="red" size="lg">
+        <div>My content</div>
     </twig:Alert>
-
-    {# will render as: #}
-
-    <div class="alert bg-red rounded-md">
-        ...
-    </div> 
+    // class="alert bg-red text-lg font-bold rounded-md"
 
 Test Helpers
 ------------
@@ -1493,12 +1553,6 @@ are called additional times, the cached value is used.
     Computed methods only work for component methods with no required
     arguments.
 
-.. tip::
-
-    Ensure to not use the ``ExposeInTemplate`` attribute on a computed method,
-    otherwise the method will be called twice instead of only once, leading to
-    unnecessary overhead and potential performance issues.
-
 Events
 ------
 
@@ -1540,6 +1594,10 @@ the twig template and twig variables before components are rendered::
 PostRenderEvent
 ~~~~~~~~~~~~~~~
 
+.. versionadded:: 2.5
+
+    The ``PostRenderEvent`` was added in TwigComponents 2.5.
+
 The ``PostRenderEvent`` is called after a component has finished
 rendering and contains the ``MountedComponent`` that was just
 rendered.
@@ -1547,6 +1605,10 @@ rendered.
 PreCreateForRenderEvent
 ~~~~~~~~~~~~~~~~~~~~~~~
 
+.. versionadded:: 2.5
+
+    The ``PreCreateForRenderEvent`` was added in TwigComponents 2.5.
+
 Subscribing to the ``PreCreateForRenderEvent`` gives the ability to be
 notified before a component object is created or hydrated, at the
 very start of the rendering process. You have access to the component
@@ -1640,14 +1702,14 @@ If a component class matches multiple namespaces, the first matched will
 be used.
 
 3rd-Party Bundle
-----------------
+~~~~~~~~~~~~~~~~
 
 The flexibility of Twig Components is extended even further when integrated
 with third-party bundles, allowing developers to seamlessly include pre-built
 components into their projects.
 
 Anonymous Components
-~~~~~~~~~~~~~~~~~~~~
+--------------------
 
 .. versionadded:: 2.20
 
@@ -1659,11 +1721,11 @@ can reference its components directly within your Twig templates:
 
 .. code-block:: html+twig
 
-    <twig:Acme:Button type="primary">
+    <twig:Shadcn:Button type="primary">
         Click me
-    </twig:Acme:Button>
+    </twig:Shadcn:Button>
 
-Here, the component name is composed of the bundle's Twig namespace ``Acme``, followed
+Here, the component name is composed of the bundle's Twig namespace ``Shadcn``, followed
 by a colon, and then the component path Button.
 
 .. note::
@@ -1672,8 +1734,8 @@ by a colon, and then the component path Button.
     ``bin/console debug:twig`` command.
 
 The component must be located in the bundle's ``templates/components/`` directory. For
-example, the component referenced as ``<twig:Acme:Button>`` should have its template
-file at ``templates/components/Button.html.twig`` within the Acme bundle.
+example, the component referenced as ``<twig:Shadcn:Button>`` should have its template
+file at ``templates/components/Button.html.twig`` within the Shadcn bundle.
 
 Debugging Components
 --------------------
@@ -1739,6 +1801,7 @@ https://symfony.com/doc/current/contributing/code/bc.html
 .. _`Stimulus controller`: https://symfony.com/bundles/StimulusBundle/current/index.html
 .. _`CVA (Class Variant Authority)`: https://cva.style/docs/getting-started/variants
 .. _`html_cva`: https://twig.symfony.com/doc/3.x/functions/html_cva.html
+.. _`shadcn/ui`: https://ui.shadcn.com
 .. _`tales-from-a-dev/twig-tailwind-extra`: https://github.com/tales-from-a-dev/twig-tailwind-extra
 .. _`ignore not defined options`: https://symfony.com/doc/current/components/options_resolver.html#ignore-not-defined-options
 .. _`Symfony MakerBundle`: https://symfony.com/bundles/SymfonyMakerBundle/current/index.html
diff --git a/ux.symfony.com/.platform.app.yaml b/ux.symfony.com/.platform.app.yaml
index 5681b6a4..e195bba0 100644
--- a/ux.symfony.com/.platform.app.yaml
+++ b/ux.symfony.com/.platform.app.yaml
@@ -4,7 +4,7 @@ type: php:8.3
 
 dependencies:
     php:
-        composer/composer: "^2.8"
+        composer/composer: "^2"
 
 build:
     flavor: none
diff --git a/ux.symfony.com/assets/styles/components/_Banner.scss b/ux.symfony.com/assets/styles/components/_Banner.scss
index aef2d0c2..7e6f4564 100644
--- a/ux.symfony.com/assets/styles/components/_Banner.scss
+++ b/ux.symfony.com/assets/styles/components/_Banner.scss
@@ -3,26 +3,10 @@
 // -----------------------------------------------------------------
 
 .Banner {
-  --color-front: #fff;
-  --color-back: #0A0A0A;
-  --color-text: color-mix(in oklab, var(--color-front) 80%, var(--color-back) 20%);
-  --color-light: color-mix(in oklab, var(--color-front) 50%, var(--color-back) 50%);
-  
-  --space-block: .5rem;
-  --space-inline: 2rem;
-  --font-size: .8rem;
-
-  @media screen and (min-width: 768px) {
-    --font-size: .9rem;
-    --space-inline: 1rem;
-  }
-
-  background: var(--color-back);
-  color: var(--color-front);
-  z-index: 10;
-  position: relative;
+  background: #0A0A0A;
+  color: #fff;
+  transition: transform 250ms linear;
 }
-
 .Banner + .App {
   position: relative;
 }
@@ -31,102 +15,84 @@
   transform: translateY(-100%);
 }
 
-.Banner_inner {
+.BannerInner {
   display: flex;
   flex-direction: row;
   flex-wrap: nowrap;
-  padding-block: var(--space-block);
-  gap: var(--space-inline);
+  padding-block: .5rem;
+  gap: 1rem;
   position: relative;
 }
 
-.Banner::after {
-  height: 2px;
-  backdrop-filter: blur(4px);
-  width: 100%;
-  content: "";
-  background: rgb(255 255 255 / .1);
-}
-
-.Banner_content {
+.BannerContent {
   flex: 1;
   display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
+  flex-direction: column;
+  place-content: center;
   gap: 0.25rem;
   text-wrap: balance;
 }
 
-.Banner_text {
+.BannerText {
+  text-align: center;
   margin: 0;
-  font-size: var(--font-size);
+  font-size: 0.8rem;
   line-height: 1.3;
-  font-weight: 300;
-  color: var(--color-text, var(--color-front));
-  .Icon {
-    transform: translateY(-12%);
-    margin-inline: calc(var(--space-inline) / 2);
-  }
+  font-weight: 400;
+  color: #fffd;
+}
+.BannerText em {
+  text-decoration: underline;
+  text-underline-offset: .25rem;
+  font-weight: 600;
+  font-style: normal;
 }
 
-.Banner_link {
-  color: var(--color-front);
-  font-weight: 400;
-  transition: all 300ms ease-out;
-  transform: none;
-  display: inline-flex;
-  text-decoration: underline 1px;
-  text-underline-offset: 0.2em;
-  text-decoration-color: var(--color-back);
-  
-  &:hover {
-    transition: all 150ms ease-out;
-    transform: translateY(-6%);
-    text-decoration-color: var(--color-text);
-    text-underline-offset: 0.3em;
-  }
-  
-  strong {
-    font-weight: 600;
-  }
+.BannerAction {
+  display: grid;
+  place-content: center;
+  justify-content: center;
 }
 
-.Banner_cta {
+.BannerButton {
   display: flex;
-  flex-direction: row;
-  gap: .75em;
   align-items: center;
-  flex-wrap: nowrap;
-  flex-shrink: 0;
-
-  span {
-    transition: all 350ms ease-out;
-  }
-
-  &:hover {
-    span {
-      transition: all 150ms linear;
-      transform: translateY(-0.1em);
-    }
-  }
-
-  .Icon {
-    width: 1.5em;
-    height: 1.5em;
-  }
+  padding: .15em .5em;
+  align-self: center;
+  color: #fffc;
+  font-size: .9em;
+  font-family: var(--font-family-title);
+  gap: .35em;
+  justify-content: center;
+  background: linear-gradient(#000e, #000d) padding-box,
+  linear-gradient(90deg, #D65831 0%, #D2D631 36.52%, #31D673 71.83%, #3aa3ff 100%) border-box;
+  border-radius: 2em;
+  border: 2px solid transparent;
+  transition: all 250ms linear;
+}
+.BannerButton:hover {
+  color: #fff;
+}
+.BannerButton:after {
+  content: "";
+  position: absolute;
+  inset: 0;
 }
 
-@media screen and (max-width: 768px) {
-  .Banner_text .Icon,
-  .Banner_text .more, 
-  .Banner_cta span {
-    display: none;
+.BannerButton svg {
+  width: 1em;
+  height: 1em;
+  color: currentColor;
+  transition: all 250ms linear;
+}
+.BannerButton:hover svg {
+  transform: scale(1.1);
+}
+@media screen and (min-width: 768px) {
+  .BannerInner {
+    padding: .5rem 2rem;
   }
-  .Banner_cta:after {
-    content: '';
-    position: absolute;
-    inset: 0;
-    z-index: 100;
+  .BannerText {
+    font-size: .9rem;
   }
 }
diff --git a/ux.symfony.com/assets/styles/components/_IconModal.scss b/ux.symfony.com/assets/styles/components/_IconModal.scss
index 5f51fa84..381d446f 100644
--- a/ux.symfony.com/assets/styles/components/_IconModal.scss
+++ b/ux.symfony.com/assets/styles/components/_IconModal.scss
@@ -15,7 +15,7 @@
   min-height: 1rem;
   max-width: 100vw;
   max-height: 100vh;
-  overflow: auto;
+  overflow: clip;
   padding: 1rem 1.5rem;
   background: var(--bs-body-bg);
   color: var(--bs-body-color);
@@ -59,6 +59,7 @@
 
 .IconModal__body {
   display: grid;
+  grid-template-columns: 300px 1fr;
   gap: 1.5rem;
 }
 
@@ -175,7 +176,7 @@
   padding: 0.35rem 0.5rem;
   border-radius: .5rem;
   border: 1px solid var(--bs-dark-border-subtle);
-  background: var(--bs-secondary-bg);
+  background: var(--bs-secondary-bg-subtle);
   overflow: hidden;
   position: relative;
   opacity: .8;
@@ -211,7 +212,7 @@
 }
 
 .IconModal__code span {
-  color: var(--bs-code-color);
+  color: lightskyblue;
 }
 
 .IconModal__copy {
@@ -238,20 +239,3 @@
   padding-top: 0;
 }
 
-.IconModal__main .TabBody {
-    padding-block-end: 0;
-}
-
-@media screen and (max-width: 768px) {
-  .IconModal {
-    padding: .5rem;
-    margin: .5rem;
-    width: auto;
-    height: auto;
-  }
-}
-@media screen and (min-width: 768px) {
-  .IconModal__body {
-    grid-template-columns: 300px 1fr;
-  }
-}
diff --git a/ux.symfony.com/assets/styles/components/_Tabs.scss b/ux.symfony.com/assets/styles/components/_Tabs.scss
index 1ca29f84..b769ac56 100644
--- a/ux.symfony.com/assets/styles/components/_Tabs.scss
+++ b/ux.symfony.com/assets/styles/components/_Tabs.scss
@@ -30,7 +30,7 @@
   transition: background-color 150ms ease-in-out;
 }
 .TabControl.active {
-  background: var(--bs-tertiary-bg);
+  background: var(--bs-secondary-bg);
 }
 
 .TabPanel {
diff --git a/ux.symfony.com/assets/styles/sections/_nav.scss b/ux.symfony.com/assets/styles/sections/_nav.scss
index d7e2040b..f0a59f06 100644
--- a/ux.symfony.com/assets/styles/sections/_nav.scss
+++ b/ux.symfony.com/assets/styles/sections/_nav.scss
@@ -61,7 +61,7 @@
 
 @media (max-width: 859px) {
   .AppNav_menu {
-    transform: translateY(-40%);
+    transform: translateY(-20%);
     opacity: 0;
     z-index: -1;
     transition: transform 350ms ease-out;
diff --git a/ux.symfony.com/templates/_banner.html.twig b/ux.symfony.com/templates/_banner.html.twig
index e54a457e..eb02f25b 100644
--- a/ux.symfony.com/templates/_banner.html.twig
+++ b/ux.symfony.com/templates/_banner.html.twig
@@ -1,22 +1,15 @@
-<aside class="Banner" style="--color-back: {{ color_back ?? '#D5054EFF' }}; --color-front: #ffffffdd;">
-    <div class="container-xxl px-4 px-md-5">
-        <div class="Banner_inner">
-            <div class="Banner_content">
-                <p class="Banner_text">
-                    <span>
-                        Get ready for an unforgettable
-                        <a href="https://live.symfony.com/2025-chicago/" class="Banner_link"><strong>SymfonyDay Chicago 2025</strong></a>!
-                        <twig:ux:icon name="si:heart-fill" />
-                    </span>
-                    <span class="more">
-                        Join us for a full day dedicated to
-                        <a href="https://gofund.me/44ecdba2" rel="external" class="Banner_link">Ryan Weaver</a>
-                        & Symfony UX!
-                    </span>
+<aside class="Banner">
+    <div class="container-xxl">
+        <div class="BannerInner">
+            <div class="BannerContent">
+                <p class="BannerText">
+                    Ryan is the lead maintainer of Symfony UX. Now he <em>needs your help</em>.
                 </p>
-                <a href="https://live.symfony.com/2025-chicago/" class="Banner_cta" rel="external">
-                    <twig:ux:icon name="ion:ticket" />
-                    <span>Get Your Tickets!</span>
+            </div>
+            <div class="BannerAction">
+                <a href="https://gofund.me/44ecdba2" class="BannerButton" rel="external">
+                    <twig:ux:icon name="heart" aria-hidden="true"/>
+                    Donate
                 </a>
             </div>
         </div>
diff --git a/ux.symfony.com/templates/icons/index.html.twig b/ux.symfony.com/templates/icons/index.html.twig
index d4580f39..eb0bbad4 100644
--- a/ux.symfony.com/templates/icons/index.html.twig
+++ b/ux.symfony.com/templates/icons/index.html.twig
@@ -48,11 +48,6 @@
         <p class="eyebrows" style="margin: 4rem 0 0;">Iconic Packages</p>
         <div class="d-md-flex justify-content-md-between text-center align-items-center text-md-start">
             <h2 class="ubuntu pt-2 component-headlines">Popular <em class="rainbow-emphasis">Icon Sets</em></h2>
-            <a style="height: 100%;" class="btn btn-md btn-outline-primary my-3 my-md-0"
-               href="https://symfony.com/bundles/ux-icons/current/index.html" rel="external" target="_blank">
-                Read Documentation
-                <twig:ux:icon name="arrow-right" style="transform: rotate(-45deg);"/>
-            </a>
         </div>
         <div class="mt-3 mt-md-5">
             <div style="display:grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 2rem;">
@@ -66,41 +61,6 @@
         </div>
     </section>
 
-    <section class="section">
-        <div class="container-fluid container-xxl px-4 px-md-5 py-3">
-
-            <div class="d-md-flex justify-content-md-between py-5 text-center align-items-center text-md-start">
-                <h2 class="ubuntu pt-2 component-headlines">Documentation</h2>
-                <a href="https://symfony.com/bundles/ux-icons/current/index.html" class="btn btn-md btn-outline-primary my-3 my-md-0"
-                   rel="external" target="_blank">
-                    Read Full Documentation
-                    <twig:ux:icon name="arrow-right" style="transform: rotate(-45deg);"/>
-                </a>
-            </div>
-
-            <div class="row">
-                <div class="col-4">
-                    <h3 class="ubuntu mb-4" style="font-weight: 400;" title="Install UX Icons">Installation</h3>
-                    <twig:Terminal bottomPadding="20">
-                        composer require {{ package.composerName }}
-                    </twig:Terminal>
-                </div>
-                <div class="col-4">
-                    <h3 class="ubuntu mb-4" style="font-weight: 400;" title="Search UX Icons">Search Icons</h3>
-                    <twig:Terminal bottomPadding="20">
-                        php bin/console ux:icons:search lucide
-                    </twig:Terminal>
-                </div>
-                <div class="col-4">
-                   <h3 class="ubuntu mb-4" style="font-weight: 400;" title="Symfony UX Bundle Configuration">Configuration</h3>
-                    <twig:Terminal bottomPadding="20">
-                        php bin/console debug:config ux_icons
-                    </twig:Terminal>
-                </div>
-            </div>
-        </div>
-    </section>
-
 {% endblock %}
 
 {% block aside %}
diff --git a/ux.symfony.com/templates/ux_packages/chartjs.html.twig b/ux.symfony.com/templates/ux_packages/chartjs.html.twig
index b13f1e56..746bde80 100644
--- a/ux.symfony.com/templates/ux_packages/chartjs.html.twig
+++ b/ux.symfony.com/templates/ux_packages/chartjs.html.twig
@@ -1,9 +1,5 @@
 {% extends 'ux_packages/package.html.twig' %}
 
-{% block banner %}
-    {{ include('_banner.html.twig', {color_back: '#165514'}) }}
-{% endblock %}
-
 {% block package_header %}
     {% component PackageHeader with {
         package: 'chartjs',
diff --git a/ux.symfony.com/templates/ux_packages/live_component.html.twig b/ux.symfony.com/templates/ux_packages/live_component.html.twig
index 20b2cfbc..b8eb5a28 100644
--- a/ux.symfony.com/templates/ux_packages/live_component.html.twig
+++ b/ux.symfony.com/templates/ux_packages/live_component.html.twig
@@ -1,9 +1,5 @@
 {% extends 'ux_packages/package.html.twig' %}
 
-{% block banner %}
-    {{ include('_banner.html.twig', {color_back: '#854e0d'}) }}
-{% endblock %}
-
 {% block package_header %}
     {% component PackageHeader with {
         package: 'live-component',
diff --git a/ux.symfony.com/templates/ux_packages/map.html.twig b/ux.symfony.com/templates/ux_packages/map.html.twig
index 7273ea33..8f7afe5e 100644
--- a/ux.symfony.com/templates/ux_packages/map.html.twig
+++ b/ux.symfony.com/templates/ux_packages/map.html.twig
@@ -1,9 +1,5 @@
 {% extends 'ux_packages/package.html.twig' %}
 
-{% block banner %}
-    {{ include('_banner.html.twig', {color_back: '#165514'}) }}
-{% endblock %}
-
 {% set package = 'map'|ux_package %}
 
 {% block package_header %}
diff --git a/ux.symfony.com/templates/ux_packages/package.html.twig b/ux.symfony.com/templates/ux_packages/package.html.twig
index 7b1c4b79..709debc3 100644
--- a/ux.symfony.com/templates/ux_packages/package.html.twig
+++ b/ux.symfony.com/templates/ux_packages/package.html.twig
@@ -17,10 +17,6 @@
     }
 } %}
 
-{% block banner %}
-    {{ include('_banner.html.twig', {color_back: '#222'}) }}
-{% endblock %}
-
 {% block header %}
     {{ include('_header.html.twig', {
         theme: 'white'
diff --git a/ux.symfony.com/templates/ux_packages/stimulus.html.twig b/ux.symfony.com/templates/ux_packages/stimulus.html.twig
index 0d3b9c65..7b11dc69 100644
--- a/ux.symfony.com/templates/ux_packages/stimulus.html.twig
+++ b/ux.symfony.com/templates/ux_packages/stimulus.html.twig
@@ -1,9 +1,5 @@
 {% extends 'ux_packages/package.html.twig' %}
 
-{% block banner %}
-    {{ include('_banner.html.twig', {color_back: '#18543e'}) }}
-{% endblock %}
-
 {% block package_header %}
     <twig:Package:PackageHeader
         package="{{ 'stimulus'|ux_package }}"
diff --git a/ux.symfony.com/templates/ux_packages/turbo.html.twig b/ux.symfony.com/templates/ux_packages/turbo.html.twig
index d16863ca..f54cacf1 100644
--- a/ux.symfony.com/templates/ux_packages/turbo.html.twig
+++ b/ux.symfony.com/templates/ux_packages/turbo.html.twig
@@ -1,9 +1,5 @@
 {% extends 'ux_packages/package.html.twig' %}
 
-{% block banner %}
-    {{ include('_banner.html.twig', {color_back: '#331C50'}) }}
-{% endblock %}
-
 {% block package_header %}
     {% component PackageHeader with {
         package: 'turbo',
diff --git a/ux.symfony.com/templates/ux_packages/twig_component.html.twig b/ux.symfony.com/templates/ux_packages/twig_component.html.twig
index bae2a5a4..91da8abb 100644
--- a/ux.symfony.com/templates/ux_packages/twig_component.html.twig
+++ b/ux.symfony.com/templates/ux_packages/twig_component.html.twig
@@ -1,9 +1,5 @@
 {% extends 'ux_packages/package.html.twig' %}
 
-{% block banner %}
-    {{ include('_banner.html.twig', {color_back: '#4a5d20'}) }}
-{% endblock %}
-
 {% block package_header %}
     {% component PackageHeader with {
         package: 'twig-component',


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index d0efd220..24dd1d62 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -17,9 +17,16 @@ jobs:
         steps:
             - uses: actions/checkout@v4
             - run: corepack enable
-            - uses: actions/setup-node@v4
+            - name: Get yarn cache directory path
+              id: yarn-cache-dir-path
+              run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
+            - uses: actions/cache@v4
+              id: yarn-cache
               with:
-                cache: 'yarn'
+                  path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+                  key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }}
+                  restore-keys: |
+                      ${{ runner.os }}-yarn-
             - run: yarn --immutable
             - run: yarn ci
 
@@ -29,24 +36,32 @@ jobs:
         steps:
             - uses: actions/checkout@v4
             - run: corepack enable
-            - uses: actions/setup-node@v4
+            - name: Get yarn cache directory path
+              id: yarn-cache-dir-path
+              run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
+            - uses: actions/cache@v4
+              id: yarn-cache
               with:
-                cache: 'yarn'
-                cache-dependency-path: |
-                  yarn.lock
-                  **/package.json
+                  path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+                  key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }}
+                  restore-keys: |
+                      ${{ runner.os }}-yarn-
             - run: yarn --immutable && yarn build
+            - name: Check if js dist files are current
+              id: changes
+              run: |
+                echo "STATUS=$(git status --porcelain)" >> $GITHUB_OUTPUT
 
-            - name: Check if JS dist files are current
+            - name: No changes found
+              if: steps.changes.outputs.STATUS == ''
+              run: |
+                echo "git status is clean"
+            - name: Changes were found
+              if: steps.changes.outputs.STATUS != ''
               run: |
-                if [[ -n $(git status --porcelain) ]]; then
-                  echo "The Git workspace is unclean! Changes detected:"
-                  git status --porcelain
-                  git diff
-                  exit 1
-                else
-                  echo "The Git workspace is clean. No changes detected."
-                fi
+                echo "JS dist files need to be rebuilt"
+                echo "${{ steps.changes.outputs.STATUS }}"
+                exit 1
 
     tests-php-components:
         runs-on: ubuntu-latest
@@ -119,12 +134,16 @@ jobs:
         steps:
             - uses: actions/checkout@v4
             - run: corepack enable
-            - uses: actions/setup-node@v4
+            - name: Get yarn cache directory path
+              id: yarn-cache-dir-path
+              run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
+            - uses: actions/cache@v4
+              id: yarn-cache
               with:
-                cache: 'yarn'
-                cache-dependency-path: |
-                  yarn.lock
-                  **/package.json
+                  path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+                  key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }}
+                  restore-keys: |
+                      ${{ runner.os }}-yarn-
             - run: yarn --immutable
             - run: yarn playwright install
             - run: yarn test
diff --git a/src/Autocomplete/assets/test/controller.test.ts b/src/Autocomplete/assets/test/controller.test.ts
index 5b72e7fb..e17dc6ae 100644
--- a/src/Autocomplete/assets/test/controller.test.ts
+++ b/src/Autocomplete/assets/test/controller.test.ts
@@ -138,80 +138,6 @@ describe('AutocompleteController', () => {
         expect(fetchMock.requests()[1].url).toEqual('/path/to/autocomplete?query=foo');
     });
 
-    it('resets when ajax URL attribute on a select element changes', async () => {
-        const { container, tomSelect } = await startAutocompleteTest(`
-            <label for="the-select">Items</label>
-            <select
-                id="the-select"
-                data-testid="main-element"
-                data-controller="autocomplete"
-                data-autocomplete-url-value="/path/to/autocomplete"
-            ></select>
-        `);
-
-        const selectElement = getByTestId(container, 'main-element') as HTMLSelectElement;
-
-        // initial Ajax request on focus
-        fetchMock.mockResponseOnce(
-            JSON.stringify({
-                results: [
-                    {
-                        value: 3,
-                        text: 'salad',
-                    },
-                ],
-            })
-        );
-
-        fetchMock.mockResponseOnce(
-            JSON.stringify({
-                results: [
-                    {
-                        value: 1,
-                        text: 'pizza',
-                    },
-                    {
-                        value: 2,
-                        text: 'popcorn',
-                    },
-                ],
-            })
-        );
-
-        const controlInput = tomSelect.control_input;
-
-        // wait for the initial Ajax request to finish
-        userEvent.click(controlInput);
-        await waitFor(() => {
-            expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(1);
-        });
-
-        controlInput.value = 'foo';
-        controlInput.dispatchEvent(new Event('input'));
-
-        await waitFor(() => {
-            expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(2);
-        });
-
-        expect(selectElement.value).toBe('');
-        tomSelect.addItem('2');
-        expect(selectElement.value).toBe('2');
-
-        selectElement.outerHTML = `
-            <select
-                id="the-select"
-                data-testid="main-element"
-                data-controller="autocomplete"
-                data-autocomplete-url-value="/path/to/autocomplete2"
-            ></select>
-        `;
-
-        // wait for the MutationObserver to flush these changes
-        await shortDelay(10);
-
-        expect(selectElement.value).toBe('');
-    });
-
     it('connect with ajax URL on an input element', async () => {
         const { container, tomSelect } = await startAutocompleteTest(`
             <label for="the-input">Items</label>
diff --git a/src/Icons/tests/Unit/IconifyTest.php b/src/Icons/tests/Unit/IconifyTest.php
index c57e37cc..c6edef0c 100644
--- a/src/Icons/tests/Unit/IconifyTest.php
+++ b/src/Icons/tests/Unit/IconifyTest.php
@@ -190,47 +190,7 @@ class IconifyTest extends TestCase
         $icons = $iconify->fetchIcons('bi', ['heart', 'bar']);
 
         $this->assertCount(2, $icons);
-        $this->assertSame(['bar', 'heart'], array_keys($icons));
-        $this->assertContainsOnlyInstancesOf(Icon::class, $icons);
-    }
-
-    public function testFetchIconsByAliases(): void
-    {
-        $iconify = new Iconify(
-            cache: new NullAdapter(),
-            endpoint: 'https://example.com',
-            http: new MockHttpClient([
-                new JsonMockResponse([
-                    'mdi' => [],
-                ]),
-                new JsonMockResponse([
-                    'aliases' => [
-                        'capsule' => [
-                            'parent' => 'pill',
-                        ],
-                        'sign' => [
-                            'parent' => 'draw',
-                        ],
-                    ],
-                    'icons' => [
-                        'pill' => [
-                            'body' => '<path d="M0 0h24v24H0z" fill="none"/>',
-                        ],
-                        'glasses' => [
-                            'body' => '<path d="M0 0h24v24H0z" fill="none"/>',
-                        ],
-                        'draw' => [
-                            'body' => '<path d="M0 0h24v24H0z" fill="none"/>',
-                        ],
-                    ],
-                ]),
-            ]),
-        );
-
-        $icons = $iconify->fetchIcons('mdi', ['capsule', 'sign', 'glasses']);
-
-        $this->assertCount(3, $icons);
-        $this->assertSame(['capsule', 'glasses', 'sign'], array_keys($icons));
+        $this->assertSame(['heart', 'bar'], array_keys($icons));
         $this->assertContainsOnlyInstancesOf(Icon::class, $icons);
     }
 
diff --git a/src/LiveComponent/tests/Unit/DependencyInjection/LiveComponentConfigurationTest.php b/src/LiveComponent/tests/Unit/DependencyInjection/LiveComponentConfigurationTest.php
deleted file mode 100644
index 72481a0e..00000000
--- a/src/LiveComponent/tests/Unit/DependencyInjection/LiveComponentConfigurationTest.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-/*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\UX\LiveComponent\Tests\Unit\DependencyInjection;
-
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
-use Symfony\Component\Config\Definition\Processor;
-use Symfony\UX\LiveComponent\DependencyInjection\LiveComponentExtension;
-
-class LiveComponentConfigurationTest extends TestCase
-{
-    public function testDefaultSecret()
-    {
-        $processor = new Processor();
-        $config = $processor->processConfiguration(new LiveComponentExtension(), []);
-
-        $this->assertEquals('%kernel.secret%', $config['secret']);
-    }
-
-    public function testEmptySecretThrows()
-    {
-        $this->expectException(InvalidConfigurationException::class);
-        $this->expectExceptionMessage('The path "live_component.secret" cannot contain an empty value, but got null.');
-
-        $processor = new Processor();
-        $config = $processor->processConfiguration(new LiveComponentExtension(), [
-            'live_component' => [
-                'secret' => null,
-            ],
-        ]);
-    }
-
-    public function testCustomSecret()
-    {
-        $processor = new Processor();
-        $config = $processor->processConfiguration(new LiveComponentExtension(), [
-            'live_component' => [
-                'secret' => 'my_secret',
-            ],
-        ]);
-
-        $this->assertEquals('my_secret', $config['secret']);
-    }
-}
diff --git a/src/LiveComponent/tests/Unit/DependencyInjection/LiveComponentExtensionTest.php b/src/LiveComponent/tests/Unit/DependencyInjection/LiveComponentExtensionTest.php
deleted file mode 100644
index b70be7ed..00000000
--- a/src/LiveComponent/tests/Unit/DependencyInjection/LiveComponentExtensionTest.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-/*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\UX\LiveComponent\Tests\Unit\DependencyInjection;
-
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
-use Symfony\UX\LiveComponent\DependencyInjection\LiveComponentExtension;
-
-class LiveComponentExtensionTest extends TestCase
-{
-    public function testKernelSecretIsUsedByDefault(): void
-    {
-        $container = $this->createContainer();
-        $container->registerExtension(new LiveComponentExtension());
-        $container->loadFromExtension('live_component', []);
-        $this->compileContainer($container);
-
-        $this->assertSame('%kernel.secret%', $container->getDefinition('ux.live_component.component_hydrator')->getArgument(4));
-        $this->assertSame('%kernel.secret%', $container->getDefinition('ux.live_component.fingerprint_calculator')->getArgument(0));
-    }
-
-    public function testCustomSecretIsUsedInDefinition(): void
-    {
-        $container = $this->createContainer();
-        $container->registerExtension(new LiveComponentExtension());
-        $container->loadFromExtension('live_component', [
-            'secret' => 'custom_secret',
-        ]);
-        $this->compileContainer($container);
-
-        $this->assertSame('custom_secret', $container->getDefinition('ux.live_component.component_hydrator')->getArgument(4));
-        $this->assertSame('custom_secret', $container->getDefinition('ux.live_component.fingerprint_calculator')->getArgument(0));
-    }
-
-    private function createContainer(): ContainerBuilder
-    {
-        $container = new ContainerBuilder(new ParameterBag([
-            'kernel.cache_dir' => __DIR__,
-            'kernel.project_dir' => __DIR__,
-            'kernel.charset' => 'UTF-8',
-            'kernel.debug' => false,
-            'kernel.bundles' => [],
-            'kernel.bundles_metadata' => [],
-        ]));
-
-        return $container;
-    }
-
-    private function compileContainer(ContainerBuilder $container): void
-    {
-        $container->getCompilerPassConfig()->setOptimizationPasses([]);
-        $container->getCompilerPassConfig()->setRemovingPasses([]);
-        $container->getCompilerPassConfig()->setAfterRemovingPasses([]);
-        $container->compile();
-    }
-}
diff --git a/src/LiveComponent/tests/Unit/LiveComponentHydratorTest.php b/src/LiveComponent/tests/Unit/LiveComponentHydratorTest.php
deleted file mode 100644
index f14ff4a7..00000000
--- a/src/LiveComponent/tests/Unit/LiveComponentHydratorTest.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-/*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\UX\LiveComponent\Tests\Unit;
-
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
-use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
-use Symfony\UX\LiveComponent\LiveComponentHydrator;
-use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
-
-final class LiveComponentHydratorTest extends TestCase
-{
-    public function testConstructWithEmptySecret(): void
-    {
-        $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionMessage('A non-empty secret is required.');
-
-        new LiveComponentHydrator(
-            [],
-            $this->createMock(PropertyAccessorInterface::class),
-            $this->createMock(LiveComponentMetadataFactory::class),
-            $this->createMock(NormalizerInterface::class),
-            '',
-        );
-    }
-}
diff --git a/src/LiveComponent/tests/Unit/Util/FingerprintCalculatorTest.php b/src/LiveComponent/tests/Unit/Util/FingerprintCalculatorTest.php
deleted file mode 100644
index 71f115d9..00000000
--- a/src/LiveComponent/tests/Unit/Util/FingerprintCalculatorTest.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-/*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\UX\LiveComponent\Tests\Unit\Util;
-
-use PHPUnit\Framework\TestCase;
-use Symfony\UX\LiveComponent\Util\FingerprintCalculator;
-
-final class FingerprintCalculatorTest extends TestCase
-{
-    public function testConstructWithEmptySecret(): void
-    {
-        $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionMessage('A non-empty secret is required.');
-
-        new FingerprintCalculator('');
-    }
-}