Repository: symfony/ux
Pull request #2191 (2024-09-21 01:02:56)

[Site] Clean demo names / tpl


Patch:
diff --git a/src/Dropzone/doc/index.rst b/src/Dropzone/doc/index.rst
index cff26ffb..f564c8ad 100644
--- a/src/Dropzone/doc/index.rst
+++ b/src/Dropzone/doc/index.rst
@@ -62,7 +62,7 @@ Symfony UX Dropzone provides a default stylesheet in order to ease
 usage. You can disable it to add your own design if you wish.
 
 In ``assets/controllers.json``, disable the default stylesheet by
-switching the ``@symfony/ux-dropzone/dist/style.min.css`` autoimport to
+switching the ``@symfony/ux-dropzone/src/style.css`` autoimport to
 ``false``:
 
 .. code-block:: json
@@ -74,7 +74,7 @@ switching the ``@symfony/ux-dropzone/dist/style.min.css`` autoimport to
                     "enabled": true,
                     "fetch": "eager",
                     "autoimport": {
-                        "@symfony/ux-dropzone/dist/style.min.css": false
+                        "@symfony/ux-dropzone/src/style.css": false
                     }
                 }
             }
diff --git a/src/Icons/composer.json b/src/Icons/composer.json
index 469effc3..14dfadb5 100644
--- a/src/Icons/composer.json
+++ b/src/Icons/composer.json
@@ -52,8 +52,7 @@
         "sort-packages": true
     },
     "conflict": {
-        "symfony/flex": "<1.13",
-        "symfony/ux-twig-component": "<2.21"
+        "symfony/flex": "<1.13"
     },
     "extra": {
         "thanks": {
diff --git a/src/Icons/config/twig_component.php b/src/Icons/config/twig_component.php
index 5e27cc81..f466f253 100644
--- a/src/Icons/config/twig_component.php
+++ b/src/Icons/config/twig_component.php
@@ -16,6 +16,15 @@ use Symfony\UX\Icons\Twig\UXIconComponentListener;
 
 return static function (ContainerConfigurator $container): void {
     $container->services()
+        ->set('.ux_icons.twig_component_listener', UXIconComponentListener::class)
+            ->args([
+                service('.ux_icons.icon_renderer'),
+            ])
+            ->tag('kernel.event_listener', [
+                'event' => 'Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent',
+                'method' => 'onPreCreateForRender',
+            ])
+
         ->set('.ux_icons.twig_component.icon', UXIconComponent::class)
             ->tag('twig.component', ['key' => 'UX:Icon'])
     ;
diff --git a/src/Icons/src/Twig/UXIconComponentListener.php b/src/Icons/src/Twig/UXIconComponentListener.php
new file mode 100644
index 00000000..8ccf4df1
--- /dev/null
+++ b/src/Icons/src/Twig/UXIconComponentListener.php
@@ -0,0 +1,43 @@
+<?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\Icons\Twig;
+
+use Symfony\UX\Icons\IconRendererInterface;
+use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;
+
+/**
+ * @author Simon André <smn.andre@gmail.com>
+ *
+ * @internal
+ */
+final class UXIconComponentListener
+{
+    public function __construct(
+        private IconRendererInterface $iconRenderer,
+    ) {
+    }
+
+    public function onPreCreateForRender(PreCreateForRenderEvent $event): void
+    {
+        if ('ux:icon' !== strtolower($event->getName())) {
+            return;
+        }
+
+        $attributes = $event->getInputProps();
+        $name = (string) $attributes['name'];
+        unset($attributes['name']);
+
+        $svg = $this->iconRenderer->renderIcon($name, $attributes);
+        $event->setRenderedString($svg);
+        $event->stopPropagation();
+    }
+}
diff --git a/src/Icons/src/Twig/UXIconRuntime.php b/src/Icons/src/Twig/UXIconRuntime.php
index edaffd8b..7276ac08 100644
--- a/src/Icons/src/Twig/UXIconRuntime.php
+++ b/src/Icons/src/Twig/UXIconRuntime.php
@@ -47,12 +47,4 @@ final class UXIconRuntime implements RuntimeExtensionInterface
             throw $e;
         }
     }
-
-    public function render(array $args = []): string
-    {
-        $name = $args['name'];
-        unset($args['name']);
-
-        return $this->renderIcon($name, $args);
-    }
 }
diff --git a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
index c45b8409..e79e6c5e 100644
--- a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
+++ b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
@@ -233,6 +233,10 @@ final class LiveComponentExtension extends Extension implements PrependExtension
             ->addTag('kernel.event_subscriber');
 
         $container->register('ux.live_component.defer_live_component_subscriber', DeferLiveComponentSubscriber::class)
+            ->setArguments([
+                new Reference('ux.twig_component.component_stack'),
+                new Reference('ux.live_component.live_controller_attributes_creator'),
+            ])
             ->addTag('kernel.event_subscriber')
         ;
 
diff --git a/src/Map/CHANGELOG.md b/src/Map/CHANGELOG.md
index 3883fe05..d4894408 100644
--- a/src/Map/CHANGELOG.md
+++ b/src/Map/CHANGELOG.md
@@ -7,9 +7,6 @@
 -   Add `ux_map` Twig function (replaces `render_map` with a more flexible 
     interface)
 -   Add `<twig:ux:map />` Twig component
--   The importmap entry `@symfony/ux-map/abstract-map-controller` can be removed
-    from your importmap, it is no longer needed. 
--   Add `Polygon` support
 
 ## 2.19
 
diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts
index f7f9ffd8..142c2f69 100644
--- a/src/Map/assets/dist/abstract_map_controller.d.ts
+++ b/src/Map/assets/dist/abstract_map_controller.d.ts
@@ -3,12 +3,11 @@ export type Point = {
     lat: number;
     lng: number;
 };
-export type MapView<Options, MarkerOptions, InfoWindowOptions, PolygonOptions> = {
+export type MapView<Options, MarkerOptions, InfoWindowOptions> = {
     center: Point | null;
     zoom: number | null;
     fitBoundsToMarkers: boolean;
     markers: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
-    polygons: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
     options: Options;
 };
 export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
@@ -18,13 +17,6 @@ export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
     rawOptions?: MarkerOptions;
     extra: Record<string, unknown>;
 };
-export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
-    infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
-    points: Array<Point>;
-    title: string | null;
-    rawOptions?: PolygonOptions;
-    extra: Record<string, unknown>;
-};
 export type InfoWindowDefinition<InfoWindowOptions> = {
     headerContent: string | null;
     content: string | null;
@@ -34,16 +26,15 @@ export type InfoWindowDefinition<InfoWindowOptions> = {
     rawOptions?: InfoWindowOptions;
     extra: Record<string, unknown>;
 };
-export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon> extends Controller<HTMLElement> {
+export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow> extends Controller<HTMLElement> {
     static values: {
         providerOptions: ObjectConstructor;
         view: ObjectConstructor;
     };
-    viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions, PolygonOptions>;
+    viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions>;
     protected map: Map;
     protected markers: Array<Marker>;
     protected infoWindows: Array<InfoWindow>;
-    protected polygons: Array<Polygon>;
     connect(): void;
     protected abstract doCreateMap({ center, zoom, options, }: {
         center: Point | null;
@@ -51,19 +42,14 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
         options: MapOptions;
     }): Map;
     createMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
-    createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
     protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
-    protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
-    protected createInfoWindow({ definition, element, }: {
-        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'] | PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
-        element: Marker | Polygon;
+    protected createInfoWindow({ definition, marker, }: {
+        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
+        marker: Marker;
     }): InfoWindow;
-    protected abstract doCreateInfoWindow({ definition, element, }: {
+    protected abstract doCreateInfoWindow({ definition, marker, }: {
         definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
-        element: Marker;
-    } | {
-        definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
-        element: Polygon;
+        marker: Marker;
     }): InfoWindow;
     protected abstract doFitBoundsToMarkers(): void;
     protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
diff --git a/src/Map/assets/dist/abstract_map_controller.js b/src/Map/assets/dist/abstract_map_controller.js
index 83cc772e..9d2e3024 100644
--- a/src/Map/assets/dist/abstract_map_controller.js
+++ b/src/Map/assets/dist/abstract_map_controller.js
@@ -5,21 +5,18 @@ class default_1 extends Controller {
         super(...arguments);
         this.markers = [];
         this.infoWindows = [];
-        this.polygons = [];
     }
     connect() {
-        const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
+        const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
         this.dispatchEvent('pre-connect', { options });
         this.map = this.doCreateMap({ center, zoom, options });
         markers.forEach((marker) => this.createMarker(marker));
-        polygons.forEach((polygon) => this.createPolygon(polygon));
         if (fitBoundsToMarkers) {
             this.doFitBoundsToMarkers();
         }
         this.dispatchEvent('connect', {
             map: this.map,
             markers: this.markers,
-            polygons: this.polygons,
             infoWindows: this.infoWindows,
         });
     }
@@ -30,17 +27,10 @@ class default_1 extends Controller {
         this.markers.push(marker);
         return marker;
     }
-    createPolygon(definition) {
-        this.dispatchEvent('polygon:before-create', { definition });
-        const polygon = this.doCreatePolygon(definition);
-        this.dispatchEvent('polygon:after-create', { polygon });
-        this.polygons.push(polygon);
-        return polygon;
-    }
-    createInfoWindow({ definition, element, }) {
-        this.dispatchEvent('info-window:before-create', { definition, element });
-        const infoWindow = this.doCreateInfoWindow({ definition, element });
-        this.dispatchEvent('info-window:after-create', { infoWindow, element });
+    createInfoWindow({ definition, marker, }) {
+        this.dispatchEvent('info-window:before-create', { definition, marker });
+        const infoWindow = this.doCreateInfoWindow({ definition, marker });
+        this.dispatchEvent('info-window:after-create', { infoWindow, marker });
         this.infoWindows.push(infoWindow);
         return infoWindow;
     }
diff --git a/src/Map/assets/package.json b/src/Map/assets/package.json
index 4cd6dd52..2561ef55 100644
--- a/src/Map/assets/package.json
+++ b/src/Map/assets/package.json
@@ -8,7 +8,8 @@
     "types": "dist/abstract_map_controller.d.ts",
     "symfony": {
         "importmap": {
-            "@hotwired/stimulus": "^3.0.0"
+            "@hotwired/stimulus": "^3.0.0",
+            "@symfony/ux-map/abstract-map-controller": "path:%PACKAGE%/dist/abstract_map_controller.js"
         }
     },
     "peerDependencies": {
diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts
index bae763cc..802b6477 100644
--- a/src/Map/assets/src/abstract_map_controller.ts
+++ b/src/Map/assets/src/abstract_map_controller.ts
@@ -2,12 +2,11 @@ import { Controller } from '@hotwired/stimulus';
 
 export type Point = { lat: number; lng: number };
 
-export type MapView<Options, MarkerOptions, InfoWindowOptions, PolygonOptions> = {
+export type MapView<Options, MarkerOptions, InfoWindowOptions> = {
     center: Point | null;
     zoom: number | null;
     fitBoundsToMarkers: boolean;
     markers: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
-    polygons: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
     options: Options;
 };
 
@@ -28,14 +27,6 @@ export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
     extra: Record<string, unknown>;
 };
 
-export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
-    infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
-    points: Array<Point>;
-    title: string | null;
-    rawOptions?: PolygonOptions;
-    extra: Record<string, unknown>;
-};
-
 export type InfoWindowDefinition<InfoWindowOptions> = {
     headerContent: string | null;
     content: string | null;
@@ -63,23 +54,20 @@ export default abstract class<
     Marker,
     InfoWindowOptions,
     InfoWindow,
-    PolygonOptions,
-    Polygon,
 > extends Controller<HTMLElement> {
     static values = {
         providerOptions: Object,
         view: Object,
     };
 
-    declare viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions, PolygonOptions>;
+    declare viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions>;
 
     protected map: Map;
     protected markers: Array<Marker> = [];
     protected infoWindows: Array<InfoWindow> = [];
-    protected polygons: Array<Polygon> = [];
 
     connect() {
-        const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
+        const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
 
         this.dispatchEvent('pre-connect', { options });
 
@@ -87,8 +75,6 @@ export default abstract class<
 
         markers.forEach((marker) => this.createMarker(marker));
 
-        polygons.forEach((polygon) => this.createPolygon(polygon));
-
         if (fitBoundsToMarkers) {
             this.doFitBoundsToMarkers();
         }
@@ -96,7 +82,6 @@ export default abstract class<
         this.dispatchEvent('connect', {
             map: this.map,
             markers: this.markers,
-            polygons: this.polygons,
             infoWindows: this.infoWindows,
         });
     }
@@ -121,29 +106,18 @@ export default abstract class<
         return marker;
     }
 
-    createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon {
-        this.dispatchEvent('polygon:before-create', { definition });
-        const polygon = this.doCreatePolygon(definition);
-        this.dispatchEvent('polygon:after-create', { polygon });
-        this.polygons.push(polygon);
-        return polygon;
-    }
-
     protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
-    protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
 
     protected createInfoWindow({
         definition,
-        element,
+        marker,
     }: {
-        definition:
-            | MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']
-            | PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
-        element: Marker | Polygon;
+        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
+        marker: Marker;
     }): InfoWindow {
-        this.dispatchEvent('info-window:before-create', { definition, element });
-        const infoWindow = this.doCreateInfoWindow({ definition, element });
-        this.dispatchEvent('info-window:after-create', { infoWindow, element });
+        this.dispatchEvent('info-window:before-create', { definition, marker });
+        const infoWindow = this.doCreateInfoWindow({ definition, marker });
+        this.dispatchEvent('info-window:after-create', { infoWindow, marker });
 
         this.infoWindows.push(infoWindow);
 
@@ -152,16 +126,11 @@ export default abstract class<
 
     protected abstract doCreateInfoWindow({
         definition,
-        element,
-    }:
-        | {
-              definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
-              element: Marker;
-          }
-        | {
-              definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
-              element: Polygon;
-          }): InfoWindow;
+        marker,
+    }: {
+        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
+        marker: Marker;
+    }): InfoWindow;
 
     protected abstract doFitBoundsToMarkers(): void;
 
diff --git a/src/Map/composer.json b/src/Map/composer.json
index 52adf9bc..a24b4fd3 100644
--- a/src/Map/composer.json
+++ b/src/Map/composer.json
@@ -42,9 +42,6 @@
         "symfony/twig-bundle": "^6.4|^7.0",
         "symfony/ux-twig-component": "^2.18"
     },
-    "conflict": {
-        "symfony/ux-twig-component": "<2.21"
-    },
     "extra": {
         "thanks": {
             "name": "symfony/ux",
diff --git a/src/Map/config/twig_component.php b/src/Map/config/twig_component.php
index 096eec4a..f09999e9 100644
--- a/src/Map/config/twig_component.php
+++ b/src/Map/config/twig_component.php
@@ -17,6 +17,15 @@ use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;
 
 return static function (ContainerConfigurator $container): void {
     $container->services()
+        ->set('.ux_map.twig_component_listener', UXMapComponentListener::class)
+            ->args([
+                service('ux_map.twig_runtime'),
+            ])
+            ->tag('kernel.event_listener', [
+                'event' => PreCreateForRenderEvent::class,
+                'method' => 'onPreCreateForRender',
+            ])
+
         ->set('.ux_map.twig_component.map', UXMapComponent::class)
             ->tag('twig.component', ['key' => 'UX:Map'])
     ;
diff --git a/src/Map/doc/index.rst b/src/Map/doc/index.rst
index be16614c..8231ea46 100644
--- a/src/Map/doc/index.rst
+++ b/src/Map/doc/index.rst
@@ -118,21 +118,8 @@ A map is created by calling ``new Map()``. You can configure the center, zoom, a
                     ),
                 )
             ;
-
-            // 3. You can also add Polygons, which represents an area enclosed by a series of `Point` instances
-            $map->addPolygon(new Polygon(
-                points: [
-                    new Point(48.8566, 2.3522),
-                    new Point(45.7640, 4.8357),
-                    new Point(43.2965, 5.3698),
-                    new Point(44.8378, -0.5792),
-                ],
-                infoWindow: new InfoWindow(
-                    content: 'Paris, Lyon, Marseille, Bordeaux',
-                ),
-            ));
-
-            // 4. And inject the map in your template to render it
+    
+            // 3. And inject the map in your template to render it
             return $this->render('contact/index.html.twig', [
                 'my_map' => $myMap,
             ]);
@@ -246,5 +233,5 @@ https://symfony.com/doc/current/contributing/code/bc.html
 .. _StimulusBundle configured in your app: https://symfony.com/bundles/StimulusBundle/current/index.html
 .. _`Google Maps`: https://github.com/symfony/ux-google-map
 .. _`Leaflet`: https://github.com/symfony/ux-leaflet-map
-.. _`Symfony UX Map Google Maps brige docs`: https://github.com/symfony/ux/blob/2.x/src/Map/src/Bridge/Google/README.md
-.. _`Symfony UX Map Leaflet bridge docs`: https://github.com/symfony/ux/blob/2.x/src/Map/src/Bridge/Leaflet/README.md
+.. _`Symfony UX Map Google Maps brige docs`: https://github.com/symfony/symfony-ux/blob/{version}/src/Map/src/Bridge/Google/README.md
+.. _`Symfony UX Map Leaflet bridge docs`: https://github.com/symfony/symfony-ux/blob/{version}/src/Map/src/Bridge/Leaflet/README.md
diff --git a/src/Map/src/Bridge/Google/CHANGELOG.md b/src/Map/src/Bridge/Google/CHANGELOG.md
index 46f7d49c..1d4c12f8 100644
--- a/src/Map/src/Bridge/Google/CHANGELOG.md
+++ b/src/Map/src/Bridge/Google/CHANGELOG.md
@@ -1,12 +1,5 @@
 # CHANGELOG
 
-## 2.20
-
-### BC Breaks
-
--   Renamed importmap entry `@symfony/ux-google-map/map-controller` to `@symfony/ux-google-map`, 
-    you will need to update your importmap.
-
 ## 2.19
 
 -   Bridge added
diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
index 5095762f..aef508dc 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
@@ -1,8 +1,8 @@
-import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
+import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
+import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller';
 import type { LoaderOptions } from '@googlemaps/js-api-loader';
 type MapOptions = Pick<google.maps.MapOptions, 'mapId' | 'gestureHandling' | 'backgroundColor' | 'disableDoubleClickZoom' | 'zoomControl' | 'zoomControlOptions' | 'mapTypeControl' | 'mapTypeControlOptions' | 'streetViewControl' | 'streetViewControlOptions' | 'fullscreenControl' | 'fullscreenControlOptions'>;
-export default class extends AbstractMapController<MapOptions, google.maps.Map, google.maps.marker.AdvancedMarkerElementOptions, google.maps.marker.AdvancedMarkerElement, google.maps.InfoWindowOptions, google.maps.InfoWindow, google.maps.PolygonOptions, google.maps.Polygon> {
+export default class extends AbstractMapController<MapOptions, google.maps.Map, google.maps.marker.AdvancedMarkerElement, google.maps.InfoWindow> {
     static values: {
         providerOptions: ObjectConstructor;
     };
@@ -15,10 +15,9 @@ export default class extends AbstractMapController<MapOptions, google.maps.Map,
         options: MapOptions;
     }): google.maps.Map;
     protected doCreateMarker(definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>): google.maps.marker.AdvancedMarkerElement;
-    protected doCreatePolygon(definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>): google.maps.Polygon;
-    protected doCreateInfoWindow({ definition, element, }: {
-        definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>['infoWindow'] | PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>['infoWindow'];
-        element: google.maps.marker.AdvancedMarkerElement | google.maps.Polygon;
+    protected doCreateInfoWindow({ definition, marker, }: {
+        definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>['infoWindow'];
+        marker: google.maps.marker.AdvancedMarkerElement;
     }): google.maps.InfoWindow;
     private createTextOrElement;
     private closeInfoWindowsExcept;
diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.js b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
index 30fbe283..5a741632 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
@@ -1,58 +1,8 @@
-import { Controller } from '@hotwired/stimulus';
+import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
 import { Loader } from '@googlemaps/js-api-loader';
 
-let default_1$1 = class default_1 extends Controller {
-    constructor() {
-        super(...arguments);
-        this.markers = [];
-        this.infoWindows = [];
-        this.polygons = [];
-    }
-    connect() {
-        const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
-        this.dispatchEvent('pre-connect', { options });
-        this.map = this.doCreateMap({ center, zoom, options });
-        markers.forEach((marker) => this.createMarker(marker));
-        polygons.forEach((polygon) => this.createPolygon(polygon));
-        if (fitBoundsToMarkers) {
-            this.doFitBoundsToMarkers();
-        }
-        this.dispatchEvent('connect', {
-            map: this.map,
-            markers: this.markers,
-            polygons: this.polygons,
-            infoWindows: this.infoWindows,
-        });
-    }
-    createMarker(definition) {
-        this.dispatchEvent('marker:before-create', { definition });
-        const marker = this.doCreateMarker(definition);
-        this.dispatchEvent('marker:after-create', { marker });
-        this.markers.push(marker);
-        return marker;
-    }
-    createPolygon(definition) {
-        this.dispatchEvent('polygon:before-create', { definition });
-        const polygon = this.doCreatePolygon(definition);
-        this.dispatchEvent('polygon:after-create', { polygon });
-        this.polygons.push(polygon);
-        return polygon;
-    }
-    createInfoWindow({ definition, element, }) {
-        this.dispatchEvent('info-window:before-create', { definition, element });
-        const infoWindow = this.doCreateInfoWindow({ definition, element });
-        this.dispatchEvent('info-window:after-create', { infoWindow, element });
-        this.infoWindows.push(infoWindow);
-        return infoWindow;
-    }
-};
-default_1$1.values = {
-    providerOptions: Object,
-    view: Object,
-};
-
 let _google;
-class default_1 extends default_1$1 {
+class default_1 extends AbstractMapController {
     async connect() {
         if (!_google) {
             _google = { maps: {} };
@@ -102,26 +52,11 @@ class default_1 extends default_1$1 {
             map: this.map,
         });
         if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, element: marker });
+            this.createInfoWindow({ definition: infoWindow, marker });
         }
         return marker;
     }
-    doCreatePolygon(definition) {
-        const { points, title, infoWindow, rawOptions = {} } = definition;
-        const polygon = new _google.maps.Polygon({
-            ...rawOptions,
-            paths: points,
-            map: this.map,
-        });
-        if (title) {
-            polygon.set('title', title);
-        }
-        if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, element: polygon });
-        }
-        return polygon;
-    }
-    doCreateInfoWindow({ definition, element, }) {
+    doCreateInfoWindow({ definition, marker, }) {
         const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
         const infoWindow = new _google.maps.InfoWindow({
             headerContent: this.createTextOrElement(headerContent),
@@ -129,34 +64,22 @@ class default_1 extends default_1$1 {
             ...otherOptions,
             ...rawOptions,
         });
-        if (element instanceof google.maps.marker.AdvancedMarkerElement) {
-            element.addListener('click', () => {
-                if (definition.autoClose) {
-                    this.closeInfoWindowsExcept(infoWindow);
-                }
-                infoWindow.open({ map: this.map, anchor: element });
+        if (definition.opened) {
+            infoWindow.open({
+                map: this.map,
+                shouldFocus: false,
+                anchor: marker,
             });
-            if (definition.opened) {
-                infoWindow.open({ map: this.map, anchor: element });
-            }
         }
-        else if (element instanceof google.maps.Polygon) {
-            element.addListener('click', (event) => {
-                if (definition.autoClose) {
-                    this.closeInfoWindowsExcept(infoWindow);
-                }
-                infoWindow.setPosition(event.latLng);
-                infoWindow.open(this.map);
-            });
-            if (definition.opened) {
-                const bounds = new google.maps.LatLngBounds();
-                element.getPath().forEach((point) => {
-                    bounds.extend(point);
-                });
-                infoWindow.setPosition(bounds.getCenter());
-                infoWindow.open({ map: this.map, anchor: element });
+        marker.addListener('click', () => {
+            if (definition.autoClose) {
+                this.closeInfoWindowsExcept(infoWindow);
             }
-        }
+            infoWindow.open({
+                map: this.map,
+                anchor: marker,
+            });
+        });
         return infoWindow;
     }
     createTextOrElement(content) {
diff --git a/src/Map/src/Bridge/Google/assets/package.json b/src/Map/src/Bridge/Google/assets/package.json
index 8b8dfdbc..e5f5ea74 100644
--- a/src/Map/src/Bridge/Google/assets/package.json
+++ b/src/Map/src/Bridge/Google/assets/package.json
@@ -18,7 +18,7 @@
         "importmap": {
             "@hotwired/stimulus": "^3.0.0",
             "@googlemaps/js-api-loader": "^1.16.6",
-            "@symfony/ux-google-map": "path:%PACKAGE%/dist/map_controller.js"
+            "@symfony/ux-google-map/map-controller": "path:%PACKAGE%/dist/map_controller.js"
         }
     },
     "peerDependencies": {
diff --git a/src/Map/src/Bridge/Google/assets/src/map_controller.ts b/src/Map/src/Bridge/Google/assets/src/map_controller.ts
index 05116d80..1e1a1a06 100644
--- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts
+++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts
@@ -7,8 +7,8 @@
  * file that was distributed with this source code.
  */
 
-import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
+import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
+import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller';
 import type { LoaderOptions } from '@googlemaps/js-api-loader';
 import { Loader } from '@googlemaps/js-api-loader';
 
@@ -33,12 +33,8 @@ let _google: typeof google;
 export default class extends AbstractMapController<
     MapOptions,
     google.maps.Map,
-    google.maps.marker.AdvancedMarkerElementOptions,
     google.maps.marker.AdvancedMarkerElement,
-    google.maps.InfoWindowOptions,
-    google.maps.InfoWindow,
-    google.maps.PolygonOptions,
-    google.maps.Polygon
+    google.maps.InfoWindow
 > {
     static values = {
         providerOptions: Object,
@@ -125,45 +121,21 @@ export default class extends AbstractMapController<
         });
 
         if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, element: marker });
+            this.createInfoWindow({ definition: infoWindow, marker });
         }
 
         return marker;
     }
 
-    protected doCreatePolygon(
-        definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>
-    ): google.maps.Polygon {
-        const { points, title, infoWindow, rawOptions = {} } = definition;
-
-        const polygon = new _google.maps.Polygon({
-            ...rawOptions,
-            paths: points,
-            map: this.map,
-        });
-
-        if (title) {
-            polygon.set('title', title);
-        }
-
-        if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, element: polygon });
-        }
-
-        return polygon;
-    }
-
     protected doCreateInfoWindow({
         definition,
-        element,
+        marker,
     }: {
-        definition:
-            | MarkerDefinition<
-                  google.maps.marker.AdvancedMarkerElementOptions,
-                  google.maps.InfoWindowOptions
-              >['infoWindow']
-            | PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>['infoWindow'];
-        element: google.maps.marker.AdvancedMarkerElement | google.maps.Polygon;
+        definition: MarkerDefinition<
+            google.maps.marker.AdvancedMarkerElementOptions,
+            google.maps.InfoWindowOptions
+        >['infoWindow'];
+        marker: google.maps.marker.AdvancedMarkerElement;
     }): google.maps.InfoWindow {
         const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
 
@@ -174,35 +146,24 @@ export default class extends AbstractMapController<
             ...rawOptions,
         });
 
-        if (element instanceof google.maps.marker.AdvancedMarkerElement) {
-            element.addListener('click', () => {
-                if (definition.autoClose) {
-                    this.closeInfoWindowsExcept(infoWindow);
-                }
-                infoWindow.open({ map: this.map, anchor: element });
+        if (definition.opened) {
+            infoWindow.open({
+                map: this.map,
+                shouldFocus: false,
+                anchor: marker,
             });
+        }
 
-            if (definition.opened) {
-                infoWindow.open({ map: this.map, anchor: element });
+        marker.addListener('click', () => {
+            if (definition.autoClose) {
+                this.closeInfoWindowsExcept(infoWindow);
             }
-        } else if (element instanceof google.maps.Polygon) {
-            element.addListener('click', (event: any) => {
-                if (definition.autoClose) {
-                    this.closeInfoWindowsExcept(infoWindow);
-                }
-                infoWindow.setPosition(event.latLng);
-                infoWindow.open(this.map);
-            });
 
-            if (definition.opened) {
-                const bounds = new google.maps.LatLngBounds();
-                element.getPath().forEach((point: google.maps.LatLng) => {
-                    bounds.extend(point);
-                });
-                infoWindow.setPosition(bounds.getCenter());
-                infoWindow.open({ map: this.map, anchor: element });
-            }
-        }
+            infoWindow.open({
+                map: this.map,
+                anchor: marker,
+            });
+        });
 
         return infoWindow;
     }
diff --git a/src/Map/src/Bridge/Google/composer.json b/src/Map/src/Bridge/Google/composer.json
index f7355c95..0160a0bd 100644
--- a/src/Map/src/Bridge/Google/composer.json
+++ b/src/Map/src/Bridge/Google/composer.json
@@ -17,7 +17,6 @@
     ],
     "require": {
         "php": ">=8.3",
-        "symfony/stimulus-bundle": "^2.18.1",
         "symfony/ux-map": "^2.19"
     },
     "require-dev": {
diff --git a/src/Map/src/Bridge/Leaflet/CHANGELOG.md b/src/Map/src/Bridge/Leaflet/CHANGELOG.md
index e380bd8e..1d4c12f8 100644
--- a/src/Map/src/Bridge/Leaflet/CHANGELOG.md
+++ b/src/Map/src/Bridge/Leaflet/CHANGELOG.md
@@ -1,12 +1,5 @@
 # CHANGELOG
 
-## 2.20
-
-### BC Breaks
-
--   Renamed importmap entry `@symfony/ux-leaflet-map/map-controller` to `@symfony/ux-leaflet-map`, 
-    you will need to update your importmap. 
-
 ## 2.19
 
 -   Bridge added
diff --git a/src/Map/src/Bridge/Leaflet/README.md b/src/Map/src/Bridge/Leaflet/README.md
index a267776d..9618f9ba 100644
--- a/src/Map/src/Bridge/Leaflet/README.md
+++ b/src/Map/src/Bridge/Leaflet/README.md
@@ -90,34 +90,6 @@ export default class extends Controller
 }
 ```
 
-## Known issues
-
-### Unable to find `leaflet/dist/leaflet.min.css` file when using Webpack Encore
-
-When using Webpack Encore with the Leaflet bridge, you may encounter the following error:
-```
-Module build failed: Module not found:
-"./node_modules/.pnpm/file+vendor+symfony+ux-leaflet-map+assets_@hotwired+stimulus@3.0.0_leaflet@1.9.4/node_modules/@symfony/ux-leaflet-map/dist/map_controller.js" contains a reference to the file "leaflet/dist/leaflet.min.css".
-This file can not be found, please check it for typos or update it if the file got moved.
-
-Entrypoint app = runtime.67292354.js 488.0777101a.js app.b75294ae.css app.0975a86d.js
-webpack compiled with 1 error
- ELIFECYCLE  Command failed with exit code 1.
-```
-
-That's because the Leaflet's Stimulus controller references the `leaflet/dist/leaflet.min.css` file, 
-which exists on [jsDelivr](https://www.jsdelivr.com/package/npm/leaflet) (used by the Symfony AssetMapper component),
-but does not in the [`leaflet` npm package](https://www.npmjs.com/package/leaflet).
-The correct path is `leaflet/dist/leaflet.css`, but it is not possible to fix it because it would break compatibility 
-with the Symfony AssetMapper component.
-
-As a workaround, you can configure Webpack Encore to add an alias for the `leaflet/dist/leaflet.min.css` file:
-```js
-Encore.addAliases({
-  'leaflet/dist/leaflet.min.css': 'leaflet/dist/leaflet.css',
-})
-```
-
 ## Resources
 
 - [Documentation](https://symfony.com/bundles/ux-map/current/index.html)
diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
index 6b32a8df..c152fb4c 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
@@ -1,8 +1,8 @@
-import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
+import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
+import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
-import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolygonOptions } from 'leaflet';
+import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions } from 'leaflet';
 type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
     tileLayer: {
         url: string;
@@ -10,7 +10,7 @@ type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
         options: Record<string, unknown>;
     };
 };
-export default class extends AbstractMapController<MapOptions, typeof L.Map, MarkerOptions, typeof L.Marker, PopupOptions, typeof L.Popup, PolygonOptions, typeof L.Polygon> {
+export default class extends AbstractMapController<MapOptions, typeof L.Map, MarkerOptions, typeof L.Marker, typeof L.Popup, PopupOptions> {
     connect(): void;
     protected dispatchEvent(name: string, payload?: Record<string, unknown>): void;
     protected doCreateMap({ center, zoom, options, }: {
@@ -19,10 +19,9 @@ export default class extends AbstractMapController<MapOptions, typeof L.Map, Mar
         options: MapOptions;
     }): L.Map;
     protected doCreateMarker(definition: MarkerDefinition): L.Marker;
-    protected doCreatePolygon(definition: PolygonDefinition): L.Polygon;
-    protected doCreateInfoWindow({ definition, element, }: {
-        definition: MarkerDefinition['infoWindow'] | PolygonDefinition['infoWindow'];
-        element: L.Marker | L.Polygon;
+    protected doCreateInfoWindow({ definition, marker, }: {
+        definition: MarkerDefinition['infoWindow'];
+        marker: L.Marker;
     }): L.Popup;
     protected doFitBoundsToMarkers(): void;
 }
diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
index 6d4a1837..466d40c5 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
@@ -1,58 +1,8 @@
-import { Controller } from '@hotwired/stimulus';
+import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
 
-class default_1 extends Controller {
-    constructor() {
-        super(...arguments);
-        this.markers = [];
-        this.infoWindows = [];
-        this.polygons = [];
-    }
-    connect() {
-        const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
-        this.dispatchEvent('pre-connect', { options });
-        this.map = this.doCreateMap({ center, zoom, options });
-        markers.forEach((marker) => this.createMarker(marker));
-        polygons.forEach((polygon) => this.createPolygon(polygon));
-        if (fitBoundsToMarkers) {
-            this.doFitBoundsToMarkers();
-        }
-        this.dispatchEvent('connect', {
-            map: this.map,
-            markers: this.markers,
-            polygons: this.polygons,
-            infoWindows: this.infoWindows,
-        });
-    }
-    createMarker(definition) {
-        this.dispatchEvent('marker:before-create', { definition });
-        const marker = this.doCreateMarker(definition);
-        this.dispatchEvent('marker:after-create', { marker });
-        this.markers.push(marker);
-        return marker;
-    }
-    createPolygon(definition) {
-        this.dispatchEvent('polygon:before-create', { definition });
-        const polygon = this.doCreatePolygon(definition);
-        this.dispatchEvent('polygon:after-create', { polygon });
-        this.polygons.push(polygon);
-        return polygon;
-    }
-    createInfoWindow({ definition, element, }) {
-        this.dispatchEvent('info-window:before-create', { definition, element });
-        const infoWindow = this.doCreateInfoWindow({ definition, element });
-        this.dispatchEvent('info-window:after-create', { infoWindow, element });
-        this.infoWindows.push(infoWindow);
-        return infoWindow;
-    }
-}
-default_1.values = {
-    providerOptions: Object,
-    view: Object,
-};
-
-class map_controller extends default_1 {
+class map_controller extends AbstractMapController {
     connect() {
         L.Marker.prototype.options.icon = L.divIcon({
             html: '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linecap="round" clip-rule="evenodd" viewBox="0 0 500 820"><defs><linearGradient id="__sf_ux_map_gradient_marker_fill" x1="0" x2="1" y1="0" y2="0" gradientTransform="matrix(0 -37.57 37.57 0 416.45 541)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#126FC6"/><stop offset="1" stop-color="#4C9CD1"/></linearGradient><linearGradient id="__sf_ux_map_gradient_marker_border" x1="0" x2="1" y1="0" y2="0" gradientTransform="matrix(0 -19.05 19.05 0 414.48 522.49)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2E6C97"/><stop offset="1" stop-color="#3883B7"/></linearGradient></defs><circle cx="252.31" cy="266.24" r="83.99" fill="#fff"/><path fill="url(#__sf_ux_map_gradient_marker_fill)" stroke="url(#__sf_ux_map_gradient_marker_border)" stroke-width="1.1" d="M416.54 503.61c-6.57 0-12.04 5.7-12.04 11.87 0 2.78 1.56 6.3 2.7 8.74l9.3 17.88 9.26-17.88c1.13-2.43 2.74-5.79 2.74-8.74 0-6.18-5.38-11.87-11.96-11.87Zm0 7.16a4.69 4.69 0 1 1-.02 9.4 4.69 4.69 0 0 1 .02-9.4Z" transform="translate(-7889.1 -9807.44) scale(19.54)"/></svg>',
@@ -88,30 +38,19 @@ class map_controller extends default_1 {
         const { position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
         const marker = L.marker(position, { title, ...otherOptions, ...rawOptions }).addTo(this.map);
         if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, element: marker });
+            this.createInfoWindow({ definition: infoWindow, marker });
         }
         return marker;
     }
-    doCreatePolygon(definition) {
-        const { points, title, infoWindow, rawOptions = {} } = definition;
-        const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map);
-        if (title) {
-            polygon.bindPopup(title);
-        }
-        if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, element: polygon });
-        }
-        return polygon;
-    }
-    doCreateInfoWindow({ definition, element, }) {
-        const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
-        element.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
+    doCreateInfoWindow({ definition, marker, }) {
+        const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
+        marker.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
         if (definition.opened) {
-            element.openPopup();
+            marker.openPopup();
         }
-        const popup = element.getPopup();
+        const popup = marker.getPopup();
         if (!popup) {
-            throw new Error('Unable to get the Popup associated with the element.');
+            throw new Error('Unable to get the Popup associated to the Marker, this should not happens.');
         }
         return popup;
     }
diff --git a/src/Map/src/Bridge/Leaflet/assets/package.json b/src/Map/src/Bridge/Leaflet/assets/package.json
index 3dd5663a..dc95b75a 100644
--- a/src/Map/src/Bridge/Leaflet/assets/package.json
+++ b/src/Map/src/Bridge/Leaflet/assets/package.json
@@ -18,7 +18,7 @@
         "importmap": {
             "@hotwired/stimulus": "^3.0.0",
             "leaflet": "^1.9.4",
-            "@symfony/ux-leaflet-map": "path:%PACKAGE%/dist/map_controller.js"
+            "@symfony/ux-leaflet-map/map-controller": "path:%PACKAGE%/dist/map_controller.js"
         }
     },
     "peerDependencies": {
diff --git a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
index 12ed1f29..ec6a78dd 100644
--- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
@@ -1,8 +1,8 @@
-import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
+import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
+import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
-import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolygonOptions } from 'leaflet';
+import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions } from 'leaflet';
 
 type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
     tileLayer: { url: string; attribution: string; options: Record<string, unknown> };
@@ -13,10 +13,8 @@ export default class extends AbstractMapController<
     typeof L.Map,
     MarkerOptions,
     typeof L.Marker,
-    PopupOptions,
     typeof L.Popup,
-    PolygonOptions,
-    typeof L.Polygon
+    PopupOptions
 > {
     connect(): void {
         L.Marker.prototype.options.icon = L.divIcon({
@@ -65,48 +63,30 @@ export default class extends AbstractMapController<
         const marker = L.marker(position, { title, ...otherOptions, ...rawOptions }).addTo(this.map);
 
         if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, element: marker });
+            this.createInfoWindow({ definition: infoWindow, marker });
         }
 
         return marker;
     }
 
-    protected doCreatePolygon(definition: PolygonDefinition): L.Polygon {
-        const { points, title, infoWindow, rawOptions = {} } = definition;
-
-        const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map);
-
-        if (title) {
-            polygon.bindPopup(title);
-        }
-
-        if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, element: polygon });
-        }
-
-        return polygon;
-    }
-
     protected doCreateInfoWindow({
         definition,
-        element,
+        marker,
     }: {
-        definition: MarkerDefinition['infoWindow'] | PolygonDefinition['infoWindow'];
-        element: L.Marker | L.Polygon;
+        definition: MarkerDefinition['infoWindow'];
+        marker: L.Marker;
     }): L.Popup {
-        const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
-
-        element.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
+        const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
 
+        marker.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
         if (definition.opened) {
-            element.openPopup();
+            marker.openPopup();
         }
 
-        const popup = element.getPopup();
+        const popup = marker.getPopup();
         if (!popup) {
-            throw new Error('Unable to get the Popup associated with the element.');
+            throw new Error('Unable to get the Popup associated to the Marker, this should not happens.');
         }
-
         return popup;
     }
 
diff --git a/src/Map/src/Bridge/Leaflet/composer.json b/src/Map/src/Bridge/Leaflet/composer.json
index 74a166e6..16aa04b5 100644
--- a/src/Map/src/Bridge/Leaflet/composer.json
+++ b/src/Map/src/Bridge/Leaflet/composer.json
@@ -17,7 +17,6 @@
     ],
     "require": {
         "php": ">=8.3",
-        "symfony/stimulus-bundle": "^2.18.1",
         "symfony/ux-map": "^2.19"
     },
     "require-dev": {
diff --git a/src/Map/src/Map.php b/src/Map/src/Map.php
index 3ab240ae..d8fdf005 100644
--- a/src/Map/src/Map.php
+++ b/src/Map/src/Map.php
@@ -30,11 +30,6 @@ final class Map
          * @var array<Marker>
          */
         private array $markers = [],
-
-        /**
-         * @var array<Polygon>
-         */
-        private array $polygons = [],
     ) {
     }
 
@@ -88,13 +83,6 @@ final class Map
         return $this;
     }
 
-    public function addPolygon(Polygon $polygon): self
-    {
-        $this->polygons[] = $polygon;
-
-        return $this;
-    }
-
     public function toArray(): array
     {
         if (!$this->fitBoundsToMarkers) {
@@ -113,7 +101,6 @@ final class Map
             'fitBoundsToMarkers' => $this->fitBoundsToMarkers,
             'options' => (object) ($this->options?->toArray() ?? []),
             'markers' => array_map(static fn (Marker $marker) => $marker->toArray(), $this->markers),
-            'polygons' => array_map(static fn (Polygon $polygon) => $polygon->toArray(), $this->polygons),
         ];
     }
 
@@ -122,7 +109,6 @@ final class Map
      *     center?: array{lat: float, lng: float},
      *     zoom?: float,
      *     markers?: list<array>,
-     *     polygons?: list<array>,
      *     fitBoundsToMarkers?: bool,
      *     options?: object,
      * } $map
@@ -147,12 +133,6 @@ final class Map
         }
         $map['markers'] = array_map(Marker::fromArray(...), $map['markers']);
 
-        $map['polygons'] ??= [];
-        if (!\is_array($map['polygons'])) {
-            throw new InvalidArgumentException('The "polygons" parameter must be an array.');
-        }
-        $map['polygons'] = array_map(Polygon::fromArray(...), $map['polygons']);
-
         return new self(...$map);
     }
 }
diff --git a/src/Map/src/Polygon.php b/src/Map/src/Polygon.php
deleted file mode 100644
index 5d474346..00000000
--- a/src/Map/src/Polygon.php
+++ /dev/null
@@ -1,70 +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\Map;
-
-use Symfony\UX\Map\Exception\InvalidArgumentException;
-
-/**
- * Represents a polygon on a map.
- *
- * @author [Pierre Svgnt]
- */
-final readonly class Polygon
-{
-    /**
-     * @param array<string, mixed> $extra Extra data, can be used by the developer to store additional information and use them later JavaScript side
-     */
-    public function __construct(
-        private array $points,
-        private ?string $title = null,
-        private ?InfoWindow $infoWindow = null,
-        private array $extra = [],
-    ) {
-    }
-
-    /**
-     * Convert the polygon to an array representation.
-     */
-    public function toArray(): array
-    {
-        return [
-            'points' => array_map(fn (Point $point) => $point->toArray(), $this->points),
-            'title' => $this->title,
-            'infoWindow' => $this->infoWindow?->toArray(),
-            'extra' => (object) $this->extra,
-        ];
-    }
-
-    /**
-     * @param array{
-     *     points: array<array{lat: float, lng: float}>,
-     *     title: string|null,
-     *     infoWindow: array<string, mixed>|null,
-     *     extra: object,
-     * } $polygon
-     *
-     * @internal
-     */
-    public static function fromArray(array $polygon): self
-    {
-        if (!isset($polygon['points'])) {
-            throw new InvalidArgumentException('The "points" parameter is required.');
-        }
-        $polygon['points'] = array_map(Point::fromArray(...), $polygon['points']);
-
-        if (isset($polygon['infoWindow'])) {
-            $polygon['infoWindow'] = InfoWindow::fromArray($polygon['infoWindow']);
-        }
-
-        return new self(...$polygon);
-    }
-}
diff --git a/src/Map/src/Twig/MapRuntime.php b/src/Map/src/Twig/MapRuntime.php
index 8b9a1ca2..62e50be8 100644
--- a/src/Map/src/Twig/MapRuntime.php
+++ b/src/Map/src/Twig/MapRuntime.php
@@ -14,7 +14,6 @@ namespace Symfony\UX\Map\Twig;
 use Symfony\UX\Map\Map;
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
-use Symfony\UX\Map\Polygon;
 use Symfony\UX\Map\Renderer\RendererInterface;
 use Twig\Extension\RuntimeExtensionInterface;
 
@@ -33,13 +32,11 @@ final class MapRuntime implements RuntimeExtensionInterface
     /**
      * @param array<string, mixed> $attributes
      * @param array<string, mixed> $markers
-     * @param array<string, mixed> $polygons
      */
     public function renderMap(
         ?Map $map = null,
         array $attributes = [],
         ?array $markers = null,
-        ?array $polygons = null,
         ?array $center = null,
         ?float $zoom = null,
     ): string {
@@ -55,9 +52,6 @@ final class MapRuntime implements RuntimeExtensionInterface
         foreach ($markers ?? [] as $marker) {
             $map->addMarker(Marker::fromArray($marker));
         }
-        foreach ($polygons ?? [] as $polygons) {
-            $map->addPolygon(Polygon::fromArray($polygons));
-        }
         if (null !== $center) {
             $map->center(Point::fromArray($center));
         }
@@ -67,12 +61,4 @@ final class MapRuntime implements RuntimeExtensionInterface
 
         return $this->renderer->renderMap($map, $attributes);
     }
-
-    public function render(array $args = []): string
-    {
-        $map = array_intersect_key($args, ['map' => 0, 'markers' => 0, 'polygons' => 0, 'center' => 1, 'zoom' => 2]);
-        $attributes = array_diff_key($args, $map);
-
-        return $this->renderMap(...$map, attributes: $attributes);
-    }
 }
diff --git a/src/Map/src/Twig/UXMapComponent.php b/src/Map/src/Twig/UXMapComponent.php
index 39e362b3..94cb6407 100644
--- a/src/Map/src/Twig/UXMapComponent.php
+++ b/src/Map/src/Twig/UXMapComponent.php
@@ -13,7 +13,6 @@ namespace Symfony\UX\Map\Twig;
 
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
-use Symfony\UX\Map\Polygon;
 
 /**
  * @author Simon André <smn.andre@gmail.com>
@@ -30,9 +29,4 @@ final class UXMapComponent
      * @var Marker[]
      */
     public array $markers;
-
-    /**
-     * @var Polygon[]
-     */
-    public array $polygons;
 }
diff --git a/src/Map/src/Twig/UXMapComponentListener.php b/src/Map/src/Twig/UXMapComponentListener.php
new file mode 100644
index 00000000..3a36e558
--- /dev/null
+++ b/src/Map/src/Twig/UXMapComponentListener.php
@@ -0,0 +1,42 @@
+<?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\Map\Twig;
+
+use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;
+
+/**
+ * @author Simon André <smn.andre@gmail.com>
+ *
+ * @internal
+ */
+final class UXMapComponentListener
+{
+    public function __construct(
+        private MapRuntime $mapRuntime,
+    ) {
+    }
+
+    public function onPreCreateForRender(PreCreateForRenderEvent $event): void
+    {
+        if ('ux:map' !== strtolower($event->getName())) {
+            return;
+        }
+
+        $attributes = $event->getInputProps();
+        $map = array_intersect_key($attributes, ['markers' => 0, 'center' => 1, 'zoom' => 2]);
+        $attributes = array_diff_key($attributes, $map);
+
+        $html = $this->mapRuntime->renderMap(...$map, attributes: $attributes);
+        $event->setRenderedString($html);
+        $event->stopPropagation();
+    }
+}
diff --git a/src/StimulusBundle/src/Dto/StimulusAttributes.php b/src/StimulusBundle/src/Dto/StimulusAttributes.php
index 5b91589a..1b477172 100644
--- a/src/StimulusBundle/src/Dto/StimulusAttributes.php
+++ b/src/StimulusBundle/src/Dto/StimulusAttributes.php
@@ -107,36 +107,54 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
 
     public function __toString(): string
     {
-        $attributes = [];
-
-        if ($this->controllers) {
-            $attributes[] = 'data-controller="'.$this->escape(implode(' ', $this->controllers)).'"';
-        }
+        $controllers = array_map(function (string $controllerName): string {
+            return $this->escapeAsHtmlAttr($controllerName);
+        }, $this->controllers);
 
-        if ($this->actions) {
-            $actions = [];
-            foreach ($this->actions as ['controllerName' => $controllerName, 'actionName' => $actionName, 'eventName' => $eventName]) {
-                $action = $this->escape($controllerName.'#'.$actionName);
-                if (null !== $eventName) {
-                    // done separately so we can escape, but avoid escaping ->
-                    $action = $this->escape($eventName).'->'.$action;
-                }
+        // done separately so we can escape, but avoid escaping ->
+        $actions = array_map(function (array $actionData): string {
+            $controllerName = $this->escapeAsHtmlAttr($actionData['controllerName']);
+            $actionName = $this->escapeAsHtmlAttr($actionData['actionName']);
+            $eventName = $actionData['eventName'];
 
-                $actions[] = $action;
+            $action = $controllerName.'#'.$actionName;
+            if (null !== $eventName) {
+                $action = $this->escapeAsHtmlAttr($eventName).'->'.$action;
             }
 
-            $attributes[] = 'data-action="'.implode(' ', $actions).'"';
+            return $action;
+        }, $this->actions);
+
+        $targets = [];
+        foreach ($this->targets as $key => $targetNamesString) {
+            $targetNames = explode(' ', $targetNamesString);
+            $targets[$key] = implode(' ', array_map(function (string $targetName): string {
+                return $this->escapeAsHtmlAttr($targetName);
+            }, $targetNames));
+        }
+
+        $attributes = [];
+
+        if ($controllers) {
+            $attributes[] = \sprintf('data-controller="%s"', implode(' ', $controllers));
         }
 
-        foreach ($this->targets as $k => $v) {
-            $attributes[] = $this->escape($k, 'html_attr').'="'.$this->escape($v).'"';
+        if ($actions) {
+            $attributes[] = \sprintf('data-action="%s"', implode(' ', $actions));
         }
 
-        foreach ($this->attributes as $k => $v) {
-            $attributes[] = $this->escape($k, 'html_attr').'="'.$this->escape($v).'"';
+        if ($targets) {
+            $attributes[] = implode(' ', array_map(function (string $key, string $value): string {
+                return \sprintf('%s="%s"', $key, $value);
+            }, array_keys($targets), $targets));
         }
 
-        return implode(' ', $attributes);
+        return rtrim(implode(' ', [
+            ...$attributes,
+            ...array_map(function (string $attribute, string $value): string {
+                return $attribute.'="'.$this->escapeAsHtmlAttr($value).'"';
+            }, array_keys($this->attributes), $this->attributes),
+        ]));
     }
 
     public function toArray(): array
@@ -175,7 +193,7 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
     {
         $escaped = [];
         foreach ($this->toArray() as $key => $value) {
-            $escaped[$key] = $this->escape($value);
+            $escaped[$key] = $this->escapeAsHtmlAttr($value);
         }
 
         return $escaped;
@@ -194,18 +212,18 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
         return (string) $value;
     }
 
-    private function escape(mixed $value, string $strategy = 'html'): string
+    private function escapeAsHtmlAttr(mixed $value): string
     {
         if (class_exists(EscaperRuntime::class)) {
-            return $this->env->getRuntime(EscaperRuntime::class)->escape($value, $strategy);
+            return $this->env->getRuntime(EscaperRuntime::class)->escape($value, 'html_attr');
         }
 
         if (method_exists(EscaperExtension::class, 'escape')) {
-            return EscaperExtension::escape($this->env, $value, $strategy);
+            return EscaperExtension::escape($this->env, $value, 'html_attr');
         }
 
         // since twig/twig 3.9.0: Using the internal "twig_escape_filter" function is deprecated.
-        return (string) twig_escape_filter($this->env, $value, $strategy);
+        return (string) twig_escape_filter($this->env, $value, 'html_attr');
     }
 
     /**
diff --git a/src/Translator/CHANGELOG.md b/src/Translator/CHANGELOG.md
index 789af7f2..b8d13254 100644
--- a/src/Translator/CHANGELOG.md
+++ b/src/Translator/CHANGELOG.md
@@ -1,9 +1,5 @@
 # CHANGELOG
 
-## 2.20.0
-
--   Add `throwWhenNotFound` function to configure the behavior when a translation is not found.
-
 ## 2.19.0
 
 -   Add configuration to filter dumped translations by domain.
diff --git a/src/Translator/assets/dist/translator.d.ts b/src/Translator/assets/dist/translator.d.ts
index 8d9f73e0..70da6bb4 100644
--- a/src/Translator/assets/dist/translator.d.ts
+++ b/src/Translator/assets/dist/translator.d.ts
@@ -21,7 +21,6 @@ export interface Message<Translations extends TranslationsType, Locale extends L
 }
 export declare function setLocale(locale: LocaleType | null): void;
 export declare function getLocale(): LocaleType;
-export declare function throwWhenNotFound(enabled: boolean): void;
 export declare function setLocaleFallbacks(localeFallbacks: Record<LocaleType, LocaleType>): void;
 export declare function getLocaleFallbacks(): Record<LocaleType, LocaleType>;
 export declare function trans<M extends Message<TranslationsType, LocaleType>, D extends DomainsOf<M>, P extends ParametersOf<M, D>>(...args: P extends NoParametersType ? [message: M, parameters?: P, domain?: RemoveIntlIcuSuffix<D>, locale?: LocaleOf<M>] : [message: M, parameters: P, domain?: RemoveIntlIcuSuffix<D>, locale?: LocaleOf<M>]): string;
diff --git a/src/Translator/assets/dist/translator_controller.js b/src/Translator/assets/dist/translator_controller.js
index 76292583..0dbb5793 100644
--- a/src/Translator/assets/dist/translator_controller.js
+++ b/src/Translator/assets/dist/translator_controller.js
@@ -220,7 +220,6 @@ function getPluralizationRule(number, locale) {
 
 let _locale = null;
 let _localeFallbacks = {};
-let _throwWhenNotFound = false;
 function setLocale(locale) {
     _locale = locale;
 }
@@ -230,9 +229,6 @@ function getLocale() {
         document.documentElement.lang ||
         'en');
 }
-function throwWhenNotFound(enabled) {
-    _throwWhenNotFound = enabled;
-}
 function setLocaleFallbacks(localeFallbacks) {
     _localeFallbacks = localeFallbacks;
 }
@@ -274,10 +270,7 @@ function trans(message, parameters = {}, domain = 'messages', locale = null) {
             return format(translations[locale], parameters, locale);
         }
     }
-    if (_throwWhenNotFound) {
-        throw new Error(`No translation message found with id "${message.id}".`);
-    }
     return message.id;
 }
 
-export { getLocale, getLocaleFallbacks, setLocale, setLocaleFallbacks, throwWhenNotFound, trans };
+export { getLocale, getLocaleFallbacks, setLocale, setLocaleFallbacks, trans };
diff --git a/src/Translator/assets/src/translator.ts b/src/Translator/assets/src/translator.ts
index 0da20f53..78e45646 100644
--- a/src/Translator/assets/src/translator.ts
+++ b/src/Translator/assets/src/translator.ts
@@ -38,7 +38,6 @@ import { format } from './formatters/formatter';
 
 let _locale: LocaleType | null = null;
 let _localeFallbacks: Record<LocaleType, LocaleType> = {};
-let _throwWhenNotFound = false;
 
 export function setLocale(locale: LocaleType | null) {
     _locale = locale;
@@ -53,10 +52,6 @@ export function getLocale(): LocaleType {
     );
 }
 
-export function throwWhenNotFound(enabled: boolean): void {
-    _throwWhenNotFound = enabled;
-}
-
 export function setLocaleFallbacks(localeFallbacks: Record<LocaleType, LocaleType>): void {
     _localeFallbacks = localeFallbacks;
 }
@@ -167,9 +162,5 @@ export function trans<
         }
     }
 
-    if (_throwWhenNotFound) {
-        throw new Error(`No translation message found with id "${message.id}".`);
-    }
-
     return message.id;
 }
diff --git a/src/Translator/doc/index.rst b/src/Translator/doc/index.rst
index 6d194ce4..614f4799 100644
--- a/src/Translator/doc/index.rst
+++ b/src/Translator/doc/index.rst
@@ -98,27 +98,6 @@ By default, the default locale is ``en`` (English) that you can configure throug
 #. Or with ``<html data-symfony-ux-translator-locale="your-locale">`` attribute
 #. Or with ``<html lang="your-locale">`` attribute
 
-Detecting missing translations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-By default, the translator will return the translation key if the translation is missing.
-
-You can change this behavior by calling ``throwWhenNotFound(true)``:
-
-.. code-block:: diff
-
-      // assets/translator.js
-
-    - import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-translator';
-    + import { trans, getLocale, setLocale, setLocaleFallbacks, throwWhenNotFound } from '@symfony/ux-translator';
-      import { localeFallbacks } from '../var/translations/configuration';
-
-      setLocaleFallbacks(localeFallbacks);
-    + throwWhenNotFound(true)
-
-      export { trans }
-      export * from '../var/translations';
-
 Importing and using translations
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/src/Translator/src/Intl/ErrorKind.php b/src/Translator/src/Intl/ErrorKind.php
index 94066bba..cabe240e 100644
--- a/src/Translator/src/Intl/ErrorKind.php
+++ b/src/Translator/src/Intl/ErrorKind.php
@@ -14,7 +14,7 @@ namespace Symfony\UX\Translator\Intl;
 /**
  * Adapted from https://github.com/formatjs/formatjs/blob/590f1f81b26934c6dc7a55fff938df5436c6f158/packages/icu-messageformat-parser/error.ts#L9-L77.
  *
- * @internal
+ * @experimental
  */
 final class ErrorKind
 {
diff --git a/src/Translator/src/Intl/IntlMessageParser.php b/src/Translator/src/Intl/IntlMessageParser.php
index 560b883e..839de42f 100644
--- a/src/Translator/src/Intl/IntlMessageParser.php
+++ b/src/Translator/src/Intl/IntlMessageParser.php
@@ -11,21 +11,16 @@
 
 namespace Symfony\UX\Translator\Intl;
 
-use Symfony\Component\String\AbstractString;
-
 use function Symfony\Component\String\s;
 
 /**
  * Adapted from https://github.com/formatjs/formatjs/blob/590f1f81b26934c6dc7a55fff938df5436c6f158/packages/icu-messageformat-parser/parser.ts.
  *
- * @internal
+ * @experimental
  */
-final class IntlMessageParser
+class IntlMessageParser
 {
-    private readonly AbstractString $message;
-    // Minor optimization, this avoid a lot of calls to `$this->message->length()`
-    private readonly int $messageLength;
-
+    private string $message;
     private Position $position;
     private bool $ignoreTag;
     private bool $requiresOtherClause;
@@ -33,8 +28,7 @@ final class IntlMessageParser
     public function __construct(
         string $message,
     ) {
-        $this->message = s($message);
-        $this->messageLength = $this->message->length();
+        $this->message = $message;
         $this->position = new Position(0, 1, 1);
         $this->ignoreTag = true;
         $this->requiresOtherClause = true;
@@ -118,14 +112,14 @@ final class IntlMessageParser
      */
     private function parseTagName(): string
     {
-        $startOffset = $this->position->offset;
+        $startOffset = $this->offset();
 
         $this->bump(); // the first tag name character
         while (!$this->isEOF() && Utils::isPotentialElementNameChar($this->char())) {
             $this->bump();
         }
 
-        return $this->message->slice($startOffset, $this->position->offset - $startOffset)->toString();
+        return s($this->message)->slice($startOffset, $this->offset() - $startOffset)->toString();
     }
 
     /**
@@ -371,7 +365,7 @@ final class IntlMessageParser
     {
         $startingPosition = clone $this->position;
 
-        $startOffset = $this->position->offset;
+        $startOffset = $this->offset();
         $value = Utils::matchIdentifierAtIndex($this->message, $startOffset);
         $endOffset = $startOffset + s($value)->length();
 
@@ -451,9 +445,9 @@ final class IntlMessageParser
                 );
 
                 // Extract style or skeleton
-                if ($styleAndLocation && ($style = s($styleAndLocation['style'] ?? ''))->startsWith('::')) {
+                if ($styleAndLocation && s($styleAndLocation['style'] ?? '')->startsWith('::')) {
                     // Skeleton starts with `::`.
-                    $skeleton = $style->slice(2)->trimStart()->toString();
+                    $skeleton = s($styleAndLocation['style'])->slice(2)->trimStart()->toString();
 
                     if ('number' === $argType) {
                         $result = $this->parseNumberSkeletonFromString(
@@ -669,7 +663,7 @@ final class IntlMessageParser
                         --$nestedBraces;
                     } else {
                         return [
-                            'val' => $this->message->slice($startPosition->offset, $this->position->offset - $startPosition->offset)->toString(),
+                            'val' => s($this->message)->slice($startPosition->offset, $this->offset() - $startPosition->offset)->toString(),
                             'err' => null,
                         ];
                     }
@@ -682,7 +676,7 @@ final class IntlMessageParser
         }
 
         return [
-            'val' => $this->message->slice($startPosition->offset, $this->position->offset - $startPosition->offset)->toString(),
+            'val' => s($this->message)->slice($startPosition->offset, $this->offset() - $startPosition->offset)->toString(),
             'err' => null,
         ];
     }
@@ -741,7 +735,7 @@ final class IntlMessageParser
                         return $result;
                     }
                     $selectorLocation = new Location($startPosition, clone $this->position);
-                    $selector = $this->message->slice($startPosition->offset, $this->position->offset - $startPosition->offset)->toString();
+                    $selector = s($this->message)->slice($startPosition->offset, $this->offset() - $startPosition->offset)->toString();
                 } else {
                     break;
                 }
@@ -870,9 +864,14 @@ final class IntlMessageParser
         ];
     }
 
+    private function offset(): int
+    {
+        return $this->position->offset;
+    }
+
     private function isEOF(): bool
     {
-        return $this->position->offset === $this->messageLength;
+        return $this->offset() === s($this->message)->length();
     }
 
     /**
@@ -883,14 +882,16 @@ final class IntlMessageParser
      */
     private function char(): int
     {
+        $message = s($this->message);
+
         $offset = $this->position->offset;
-        if ($offset >= $this->messageLength) {
+        if ($offset >= $message->length()) {
             throw new \OutOfBoundsException();
         }
 
-        $code = $this->message->codePointsAt($offset)[0] ?? null;
+        $code = $message->slice($offset, 1)->codePointsAt(0)[0] ?? null;
         if (null === $code) {
-            throw new \Exception("Offset {$offset} is at invalid UTF-16 code unit boundary.");
+            throw new \Exception("Offset {$offset} is at invalid UTF-16 code unit boundary");
         }
 
         return $code;
@@ -908,7 +909,7 @@ final class IntlMessageParser
             'err' => [
                 'kind' => $kind,
                 'location' => $location,
-                'message' => $this->message->toString(),
+                'message' => $this->message,
             ],
         ];
     }
@@ -940,7 +941,7 @@ final class IntlMessageParser
      */
     private function bumpIf(string $prefix): bool
     {
-        if ($this->message->slice($this->position->offset)->startsWith($prefix)) {
+        if (s($this->message)->slice($this->offset())->startsWith($prefix)) {
             for ($i = 0, $len = \strlen($prefix); $i < $len; ++$i) {
                 $this->bump();
             }
@@ -957,13 +958,14 @@ final class IntlMessageParser
      */
     private function bumpUntil(string $pattern): bool
     {
-        $index = $this->message->indexOf($pattern, $this->position->offset);
+        $currentOffset = $this->offset();
+        $index = s($this->message)->indexOf($pattern, $currentOffset);
         if ($index >= 0) {
             $this->bumpTo($index);
 
             return true;
         } else {
-            $this->bumpTo($this->messageLength);
+            $this->bumpTo(s($this->message)->length());
 
             return false;
         }
@@ -977,18 +979,18 @@ final class IntlMessageParser
      */
     private function bumpTo(int $targetOffset)
     {
-        if ($this->position->offset > $targetOffset) {
-            throw new \Exception(\sprintf('targetOffset "%s" must be greater than or equal to the current offset %d', $targetOffset, $this->position->offset));
+        if ($this->offset() > $targetOffset) {
+            throw new \Exception(\sprintf('targetOffset %s must be greater than or equal to the current offset %d', $targetOffset, $this->offset()));
         }
 
-        $targetOffset = min($targetOffset, $this->messageLength);
+        $targetOffset = min($targetOffset, s($this->message)->length());
         while (true) {
-            $offset = $this->position->offset;
+            $offset = $this->offset();
             if ($offset === $targetOffset) {
                 break;
             }
             if ($offset > $targetOffset) {
-                throw new \Exception("targetOffset {$targetOffset} is at invalid UTF-16 code unit boundary.");
+                throw new \Exception("targetOffset {$targetOffset} is at invalid UTF-16 code unit boundary");
             }
 
             $this->bump();
@@ -1017,7 +1019,8 @@ final class IntlMessageParser
         }
 
         $code = $this->char();
-        $nextCodes = $this->message->codePointsAt($this->position->offset + ($code >= 0x10000 ? 2 : 1));
+        $offset = $this->offset();
+        $nextCodes = s($this->message)->codePointsAt($offset + ($code >= 0x10000 ? 2 : 1));
 
         return $nextCodes[0] ?? null;
     }
diff --git a/src/Translator/src/Intl/Location.php b/src/Translator/src/Intl/Location.php
index 270a1bfe..e342a2ff 100644
--- a/src/Translator/src/Intl/Location.php
+++ b/src/Translator/src/Intl/Location.php
@@ -14,7 +14,7 @@ namespace Symfony\UX\Translator\Intl;
 /**
  * Adapted from https://github.com/formatjs/formatjs/blob/590f1f81b26934c6dc7a55fff938df5436c6f158/packages/icu-messageformat-parser/types.ts#L58-L61.
  *
- * @internal
+ * @experimental
  */
 final class Location
 {
diff --git a/src/Translator/src/Intl/Position.php b/src/Translator/src/Intl/Position.php
index 0772508c..bc633622 100644
--- a/src/Translator/src/Intl/Position.php
+++ b/src/Translator/src/Intl/Position.php
@@ -14,7 +14,7 @@ namespace Symfony\UX\Translator\Intl;
 /**
  * Adapted from https://github.com/formatjs/formatjs/blob/590f1f81b26934c6dc7a55fff938df5436c6f158/packages/icu-messageformat-parser/types.ts#L53-L57.
  *
- * @internal
+ * @experimental
  */
 final class Position
 {
diff --git a/src/Translator/src/Intl/SkeletonType.php b/src/Translator/src/Intl/SkeletonType.php
index 089c87fe..b10daeb0 100644
--- a/src/Translator/src/Intl/SkeletonType.php
+++ b/src/Translator/src/Intl/SkeletonType.php
@@ -14,7 +14,7 @@ namespace Symfony\UX\Translator\Intl;
 /**
  * Adapted from https://github.com/formatjs/formatjs/blob/590f1f81b26934c6dc7a55fff938df5436c6f158/packages/icu-messageformat-parser/types.ts#L48-L51.
  *
- * @internal
+ * @experimental
  */
 final class SkeletonType
 {
diff --git a/src/Translator/src/Intl/Type.php b/src/Translator/src/Intl/Type.php
index d02bab4a..12927c34 100644
--- a/src/Translator/src/Intl/Type.php
+++ b/src/Translator/src/Intl/Type.php
@@ -14,7 +14,7 @@ namespace Symfony\UX\Translator\Intl;
 /**
  * Adapted from https://github.com/formatjs/formatjs/blob/590f1f81b26934c6dc7a55fff938df5436c6f158/packages/icu-messageformat-parser/types.ts#L8-L46.
  *
- * @internal
+ * @experimental
  */
 final class Type
 {
diff --git a/src/Translator/src/Intl/Utils.php b/src/Translator/src/Intl/Utils.php
index b5c44a5f..af363073 100644
--- a/src/Translator/src/Intl/Utils.php
+++ b/src/Translator/src/Intl/Utils.php
@@ -11,10 +11,10 @@
 
 namespace Symfony\UX\Translator\Intl;
 
-use Symfony\Component\String\AbstractString;
+use function Symfony\Component\String\s;
 
 /**
- * @internal
+ * @experimental
  */
 final class Utils
 {
@@ -355,12 +355,12 @@ final class Utils
         return $elements;
     }
 
-    public static function matchIdentifierAtIndex(AbstractString $s, int $index): string
+    public static function matchIdentifierAtIndex(string $s, int $index): string
     {
         $match = [];
 
         while (true) {
-            $c = $s->codePointsAt($index)[0] ?? null;
+            $c = s($s)->codePointsAt($index)[0] ?? null;
             if (null === $c || self::isWhiteSpace($c) || self::isPatternSyntax($c)) {
                 break;
             }
diff --git a/src/Translator/src/MessageParameters/Extractor/ExtractorInterface.php b/src/Translator/src/MessageParameters/Extractor/ExtractorInterface.php
index 18c709c3..93d2b1d5 100644
--- a/src/Translator/src/MessageParameters/Extractor/ExtractorInterface.php
+++ b/src/Translator/src/MessageParameters/Extractor/ExtractorInterface.php
@@ -14,7 +14,7 @@ namespace Symfony\UX\Translator\MessageParameters\Extractor;
 /**
  * @author Hugo Alliaume <hugo@alliau.me>
  *
- * @internal
+ * @experimental
  */
 interface ExtractorInterface
 {
diff --git a/src/Translator/src/MessageParameters/Extractor/IntlMessageParametersExtractor.php b/src/Translator/src/MessageParameters/Extractor/IntlMessageParametersExtractor.php
index 091e2789..6722f882 100644
--- a/src/Translator/src/MessageParameters/Extractor/IntlMessageParametersExtractor.php
+++ b/src/Translator/src/MessageParameters/Extractor/IntlMessageParametersExtractor.php
@@ -17,7 +17,7 @@ use Symfony\UX\Translator\Intl\Type;
 /**
  * @author Hugo Alliaume <hugo@alliau.me>
  *
- * @internal
+ * @experimental
  */
 final class IntlMessageParametersExtractor implements ExtractorInterface
 {
diff --git a/src/Translator/src/MessageParameters/Extractor/MessageParametersExtractor.php b/src/Translator/src/MessageParameters/Extractor/MessageParametersExtractor.php
index 38988ea7..289c32db 100644
--- a/src/Translator/src/MessageParameters/Extractor/MessageParametersExtractor.php
+++ b/src/Translator/src/MessageParameters/Extractor/MessageParametersExtractor.php
@@ -14,7 +14,7 @@ namespace Symfony\UX\Translator\MessageParameters\Extractor;
 /**
  * @author Hugo Alliaume <hugo@alliau.me>
  *
- * @internal
+ * @experimental
  */
 final class MessageParametersExtractor implements ExtractorInterface
 {
diff --git a/src/Translator/src/MessageParameters/Printer/TypeScriptMessageParametersPrinter.php b/src/Translator/src/MessageParameters/Printer/TypeScriptMessageParametersPrinter.php
index 2981142e..4eaf9c89 100644
--- a/src/Translator/src/MessageParameters/Printer/TypeScriptMessageParametersPrinter.php
+++ b/src/Translator/src/MessageParameters/Printer/TypeScriptMessageParametersPrinter.php
@@ -14,7 +14,7 @@ namespace Symfony\UX\Translator\MessageParameters\Printer;
 /**
  * @author Hugo Alliaume <hugo@alliau.me>
  *
- * @internal
+ * @experimental
  */
 final class TypeScriptMessageParametersPrinter
 {
diff --git a/src/Translator/src/TranslationsDumper.php b/src/Translator/src/TranslationsDumper.php
index a986c7c0..cba61117 100644
--- a/src/Translator/src/TranslationsDumper.php
+++ b/src/Translator/src/TranslationsDumper.php
@@ -155,18 +155,21 @@ TS
                 }
 
                 $parametersTypes[$domain] = $this->typeScriptMessageParametersPrinter->print($parameters);
+
                 $locales[] = $locale;
             }
         }
 
-        $typeScriptParametersType = [];
-        foreach ($parametersTypes as $domain => $parametersType) {
-            $typeScriptParametersType[] = \sprintf("'%s': { parameters: %s }", $domain, $parametersType);
-        }
-
         return \sprintf(
             'Message<{ %s }, %s>',
-            implode(', ', $typeScriptParametersType),
+            implode(', ', array_reduce(
+                array_keys($parametersTypes),
+                fn (array $carry, string $domain) => [
+                    ...$carry,
+                    \sprintf("'%s': { parameters: %s }", $domain, $parametersTypes[$domain]),
+                ],
+                [],
+            )),
             implode('|', array_map(fn (string $locale) => "'$locale'", array_unique($locales))),
         );
     }
@@ -186,14 +189,13 @@ TS
     {
         static $alreadyGenerated = [];
 
-        $translationId = s($translationId)->ascii()->snake()->upper()->replaceMatches('/^(\d)/', '_$1')->toString();
         $prefix = 0;
         do {
-            $constantName = $translationId.($prefix > 0 ? '_'.$prefix : '');
+            $constantName = s($translationId)->ascii()->snake()->upper()->replaceMatches('/^(\d)/', '_$1')->toString().($prefix > 0 ? '_'.$prefix : '');
             ++$prefix;
-        } while ($alreadyGenerated[$constantName] ?? false);
+        } while (\in_array($constantName, $alreadyGenerated, true));
 
-        $alreadyGenerated[$constantName] = true;
+        $alreadyGenerated[] = $constantName;
 
         return $constantName;
     }
diff --git a/src/TwigComponent/doc/index.rst b/src/TwigComponent/doc/index.rst
index ac0bf998..cb6e22ad 100644
--- a/src/TwigComponent/doc/index.rst
+++ b/src/TwigComponent/doc/index.rst
@@ -1618,15 +1618,15 @@ when using nested components:
     </twig:Card>
 
     {# ✅ this also works because it only uses the Twig syntax #}
-    {% component Card %}
+    <twig:Card>
         {# ... #}
 
-        {% block footer %}
+        <twig:block name="footer">
             {% component 'Button:Primary' with {isBlock: true} %}
                 {% block content %}Edit{% endblock %}
             {% endcomponent %}
-        {% endblock %}
-    {% endcomponent %}
+        </twig:block>
+    </twig:Card>
 
 If you're using `Live Components`_, then there *are* some guidelines related to
 how the re-rendering of parent and child components works. Read `Live Nested Components`_.
diff --git a/src/TwigComponent/src/ComponentFactory.php b/src/TwigComponent/src/ComponentFactory.php
index d797c399..2a677d38 100644
--- a/src/TwigComponent/src/ComponentFactory.php
+++ b/src/TwigComponent/src/ComponentFactory.php
@@ -41,28 +41,20 @@ final class ComponentFactory
 
     public function metadataFor(string $name): ComponentMetadata
     {
-        if ($config = $this->config[$name] ?? null) {
-            return new ComponentMetadata($config);
-        }
-
-        if ($template = $this->componentTemplateFinder->findAnonymousComponentTemplate($name)) {
-            $this->config[$name] = [
-                'key' => $name,
-                'template' => $template,
-            ];
-
-            return new ComponentMetadata($this->config[$name]);
-        }
-
-        if ($mappedName = $this->classMap[$name] ?? null) {
-            if ($config = $this->config[$mappedName] ?? null) {
-                return new ComponentMetadata($config);
+        $name = $this->classMap[$name] ?? $name;
+
+        if (!$config = $this->config[$name] ?? null) {
+            if (($template = $this->componentTemplateFinder->findAnonymousComponentTemplate($name)) !== null) {
+                return new ComponentMetadata([
+                    'key' => $name,
+                    'template' => $template,
+                ]);
             }
 
-            throw new \InvalidArgumentException(\sprintf('Unknown component "%s".', $name));
+            $this->throwUnknownComponentException($name);
         }
 
-        $this->throwUnknownComponentException($name);
+        return new ComponentMetadata($config);
     }
 
     /**
@@ -70,13 +62,11 @@ final class ComponentFactory
      */
     public function create(string $name, array $data = []): MountedComponent
     {
-        $metadata = $this->metadataFor($name);
-
-        if ($metadata->isAnonymous()) {
-            return $this->mountFromObject(new AnonymousComponent(), $data, $metadata);
-        }
-
-        return $this->mountFromObject($this->components->get($metadata->getName()), $data, $metadata);
+        return $this->mountFromObject(
+            $this->getComponent($name),
+            $data,
+            $this->metadataFor($name)
+        );
     }
 
     /**
@@ -111,7 +101,10 @@ final class ComponentFactory
         foreach ($data as $key => $value) {
             if ($value instanceof \Stringable) {
                 $data[$key] = (string) $value;
+                continue;
             }
+
+            $data[$key] = $value;
         }
 
         return new MountedComponent(
@@ -125,18 +118,10 @@ final class ComponentFactory
 
     /**
      * Returns the "unmounted" component.
-     *
-     * @internal
      */
     public function get(string $name): object
     {
-        $metadata = $this->metadataFor($name);
-
-        if ($metadata->isAnonymous()) {
-            return new AnonymousComponent();
-        }
-
-        return $this->components->get($metadata->getName());
+        return $this->getComponent($name);
     }
 
     private function mount(object $component, array &$data): void
@@ -174,6 +159,21 @@ final class ComponentFactory
         $component->mount(...$parameters);
     }
 
+    private function getComponent(string $name): object
+    {
+        $name = $this->classMap[$name] ?? $name;
+
+        if (!$this->components->has($name)) {
+            if ($this->isAnonymousComponent($name)) {
+                return new AnonymousComponent();
+            }
+
+            $this->throwUnknownComponentException($name);
+        }
+
+        return $this->components->get($name);
+    }
+
     private function preMount(object $component, array $data, ComponentMetadata $componentMetadata): array
     {
         $event = new PreMountEvent($component, $data, $componentMetadata);
@@ -215,6 +215,11 @@ final class ComponentFactory
         ];
     }
 
+    private function isAnonymousComponent(string $name): bool
+    {
+        return null !== $this->componentTemplateFinder->findAnonymousComponentTemplate($name);
+    }
+
     /**
      * @return never
      */
diff --git a/src/TwigComponent/src/ComponentMetadata.php b/src/TwigComponent/src/ComponentMetadata.php
index b71e3491..a152417a 100644
--- a/src/TwigComponent/src/ComponentMetadata.php
+++ b/src/TwigComponent/src/ComponentMetadata.php
@@ -57,11 +57,6 @@ final class ComponentMetadata
         return $this->get('expose_public_props', false);
     }
 
-    public function isAnonymous(): bool
-    {
-        return !isset($this->config['service_id']);
-    }
-
     public function getAttributesVar(): string
     {
         return $this->get('attributes_var', 'attributes');
diff --git a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php
index aa1419ee..c0c18b0e 100644
--- a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php
+++ b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php
@@ -36,8 +36,6 @@ use Symfony\UX\TwigComponent\Twig\ComponentExtension;
 use Symfony\UX\TwigComponent\Twig\ComponentLexer;
 use Symfony\UX\TwigComponent\Twig\ComponentRuntime;
 use Symfony\UX\TwigComponent\Twig\TwigEnvironmentConfigurator;
-use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
-use function Symfony\Component\DependencyInjection\Loader\Configurator\service_locator;
 
 /**
  * @author Kevin Bond <kevinbond@gmail.com>
@@ -110,10 +108,6 @@ final class TwigComponentExtension extends Extension implements ConfigurationInt
         $container->register('.ux.twig_component.twig.component_runtime', ComponentRuntime::class)
             ->setArguments([
                 new Reference('ux.twig_component.component_renderer'),
-                service_locator([
-                    'ux:icon' => service('.ux_icons.twig_icon_runtime')->nullOnInvalid(),
-                    'ux:map' => service('ux_map.twig_runtime')->nullOnInvalid(),
-                ]),
             ])
             ->addTag('twig.runtime')
         ;
diff --git a/src/TwigComponent/src/Twig/ComponentRuntime.php b/src/TwigComponent/src/Twig/ComponentRuntime.php
index f770ef32..b6157599 100644
--- a/src/TwigComponent/src/Twig/ComponentRuntime.php
+++ b/src/TwigComponent/src/Twig/ComponentRuntime.php
@@ -11,7 +11,6 @@
 
 namespace Symfony\UX\TwigComponent\Twig;
 
-use Symfony\Component\DependencyInjection\ServiceLocator;
 use Symfony\UX\TwigComponent\ComponentRenderer;
 use Symfony\UX\TwigComponent\Event\PreRenderEvent;
 
@@ -25,13 +24,15 @@ final class ComponentRuntime
 {
     public function __construct(
         private readonly ComponentRenderer $renderer,
-        private readonly ServiceLocator $renderers,
     ) {
     }
 
-    public function finishEmbedComponent(): void
+    /**
+     * @param array<string, mixed> $props
+     */
+    public function render(string $name, array $props = []): string
     {
-        $this->renderer->finishEmbeddedComponentRender();
+        return $this->renderer->createAndRender($name, $props);
     }
 
     /**
@@ -42,15 +43,6 @@ final class ComponentRuntime
         return $this->renderer->preCreateForRender($name, $props);
     }
 
-    public function render(string $name, array $props = []): string
-    {
-        if ($this->renderers->has($normalized = strtolower($name))) {
-            return $this->renderers->get($normalized)->render($props);
-        }
-
-        return $this->renderer->createAndRender($name, $props);
-    }
-
     /**
      * @param array<string, mixed> $props
      * @param array<string, mixed> $context
@@ -59,4 +51,9 @@ final class ComponentRuntime
     {
         return $this->renderer->startEmbeddedComponentRender($name, $props, $context, $hostTemplateName, $index);
     }
+
+    public function finishEmbedComponent(): void
+    {
+        $this->renderer->finishEmbeddedComponentRender();
+    }
 }
diff --git a/ux.symfony.com/assets/styles/app.scss b/ux.symfony.com/assets/styles/app.scss
index 81a8c3d2..c38c96d2 100644
--- a/ux.symfony.com/assets/styles/app.scss
+++ b/ux.symfony.com/assets/styles/app.scss
@@ -125,6 +125,7 @@ $utilities: map-remove(
 // Components
 @import "components/Badge";
 @import "components/Banner";
+@import "components/Button";
 @import "components/Browser";
 @import "components/Changelog";
 @import "components/DataList";
diff --git a/ux.symfony.com/assets/styles/components/_Button.scss b/ux.symfony.com/assets/styles/components/_Button.scss
new file mode 100644
index 00000000..79976129
--- /dev/null
+++ b/ux.symfony.com/assets/styles/components/_Button.scss
@@ -0,0 +1,19 @@
+.Button {
+  display: inline-flex;
+  align-items: center;
+  justify-items: flex-end;
+
+  border-radius: 1.5rem;
+  padding: 0.75rem 1.5rem;
+  gap: .75rem;
+
+  background: var(--bs-body-bg);
+  color: var(--bs-body-color);
+}
+
+.Button--dark {
+  background: var(--bs-body-color);
+  color: var(--bs-body-bg);
+  color: #dee2e6;
+  border: 1px solid #a6a0a0;
+}
diff --git a/ux.symfony.com/assets/styles/sections/_hero.scss b/ux.symfony.com/assets/styles/sections/_hero.scss
index 46f8a9d9..fbf30e7a 100644
--- a/ux.symfony.com/assets/styles/sections/_hero.scss
+++ b/ux.symfony.com/assets/styles/sections/_hero.scss
@@ -2,15 +2,6 @@
   padding-top: 5rem;
 }
 
-.rainbow-emphasis {
-  background: linear-gradient(113.84deg, #D65831 0%, #D2D631 36.52%, #31D673 71.83%, #315FD6 100%);
-  -webkit-background-clip: text;
-  -webkit-text-fill-color: transparent;
-  font-style: normal;
-  filter: brightness(102%) contrast(200%) drop-shadow(1px 1px 1px #0004);
-  letter-spacing: 0;
-}
-
 .hero-background {
   padding-block-start: 4rem;
 }
diff --git a/ux.symfony.com/importmap.php b/ux.symfony.com/importmap.php
index 90273c85..af8af902 100644
--- a/ux.symfony.com/importmap.php
+++ b/ux.symfony.com/importmap.php
@@ -190,6 +190,9 @@ return [
     'chart.js' => [
         'version' => '4.4.3',
     ],
+    '@symfony/ux-map/abstract-map-controller' => [
+        'path' => './vendor/symfony/ux-map/assets/dist/abstract_map_controller.js',
+    ],
     'leaflet' => [
         'version' => '1.9.4',
     ],
@@ -197,7 +200,7 @@ return [
         'version' => '1.9.4',
         'type' => 'css',
     ],
-    '@symfony/ux-leaflet-map' => [
+    '@symfony/ux-leaflet-map/map-controller' => [
         'path' => './vendor/symfony/ux-leaflet-map/assets/dist/map_controller.js',
     ],
 ];
diff --git a/ux.symfony.com/src/Controller/Demo/LiveComponentDemoController.php b/ux.symfony.com/src/Controller/Demo/LiveDemoController.php
similarity index 68%
rename from ux.symfony.com/src/Controller/Demo/LiveComponentDemoController.php
rename to ux.symfony.com/src/Controller/Demo/LiveDemoController.php
index c73d2adb..afbfb9cb 100644
--- a/ux.symfony.com/src/Controller/Demo/LiveComponentDemoController.php
+++ b/ux.symfony.com/src/Controller/Demo/LiveDemoController.php
@@ -24,7 +24,7 @@ use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Attribute\Route;
 
 #[Route('/demos/live-component')]
-class LiveComponentDemoController extends AbstractController
+class LiveDemoController extends AbstractController
 {
     #[Route('/', name: 'app_demo_live_component')]
     public function __invoke(): Response
@@ -32,14 +32,6 @@ class LiveComponentDemoController extends AbstractController
         return $this->redirectToRoute('app_demos');
     }
 
-    #[Route('/auto-validating-form', name: 'app_demo_live_component_auto_validating_form')]
-    public function demoAutoValidatingForm(LiveDemoRepository $liveDemoRepository): Response
-    {
-        return $this->render('demos/live_component/auto_validating_form.html.twig', [
-            'demo' => $liveDemoRepository->find('auto-validating-form'),
-        ]);
-    }
-
     #[Route('/form-collection-type/{id}', name: 'app_demo_live_component_form_collection_type', defaults: ['id' => null])]
     public function demoFormCollectionType(LiveDemoRepository $liveDemoRepository, Request $request, TodoListRepository $todoListRepository, ?TodoList $todoList = null): Response
     {
@@ -66,14 +58,6 @@ class LiveComponentDemoController extends AbstractController
         ]);
     }
 
-    #[Route('/dependent-form-fields', name: 'app_demo_live_component_dependent_form_fields')]
-    public function demoDependentFormFields(LiveDemoRepository $liveDemoRepository): Response
-    {
-        return $this->render('demos/live_component/dependent_form_fields.html.twig', [
-            'demo' => $liveDemoRepository->find('dependent-form-fields'),
-        ]);
-    }
-
     #[Route('/voting', name: 'app_demo_live_component_voting')]
     public function demoVoting(LiveDemoRepository $liveDemoRepository, FoodRepository $foodRepository): Response
     {
@@ -97,14 +81,6 @@ class LiveComponentDemoController extends AbstractController
         ]);
     }
 
-    #[Route('/chartjs', name: 'app_demo_live_component_chartjs')]
-    public function chartJs(LiveDemoRepository $liveDemoRepository): Response
-    {
-        return $this->render('demos/live_component/chartjs.html.twig', parameters: [
-            'demo' => $liveDemoRepository->find('chartjs'),
-        ]);
-    }
-
     #[Route('/invoice/{id}', name: 'app_demo_live_component_invoice', defaults: ['id' => null])]
     public function invoice(LiveDemoRepository $liveDemoRepository, ?Invoice $invoice = null): Response
     {
@@ -116,35 +92,20 @@ class LiveComponentDemoController extends AbstractController
         ]);
     }
 
+    #[Route('/{demo}', name: 'app_demo_live_component_demo')]
+    #[Route('/auto-validating-form', name: 'app_demo_live_component_auto_validating_form')]
+    #[Route('/chartjs', name: 'app_demo_live_component_chartjs')]
+    #[Route('/dependent-form-fields', name: 'app_demo_live_component_dependent_form_fields')]
     #[Route('/infinite-scroll', name: 'app_demo_live_component_infinite_scroll')]
-    public function infiniteScroll(LiveDemoRepository $liveDemoRepository): Response
-    {
-        return $this->render('demos/live_component/infinite_scroll.html.twig', parameters: [
-            'demo' => $liveDemoRepository->find('infinite-scroll'),
-        ]);
-    }
-
     #[Route('/infinite-scroll-2', name: 'app_demo_live_component_infinite_scroll_2')]
-    public function infiniteScroll2(LiveDemoRepository $liveDemoRepository): Response
-    {
-        return $this->render('demos/live_component/infinite_scroll_2.html.twig', parameters: [
-            'demo' => $liveDemoRepository->find('infinite-scroll-2'),
-        ]);
-    }
-
     #[Route('/product-form', name: 'app_demo_live_component_product_form')]
-    public function productForm(LiveDemoRepository $liveDemoRepository): Response
-    {
-        return $this->render('demos/live_component/product_form.html.twig', parameters: [
-            'demo' => $liveDemoRepository->find('product-form'),
-        ]);
-    }
-
     #[Route('/upload', name: 'app_demo_live_component_upload')]
-    public function uploadFiles(LiveDemoRepository $liveDemoRepository): Response
-    {
-        return $this->render('demos/live_component/upload.html.twig', parameters: [
-            'demo' => $liveDemoRepository->find('upload'),
-        ]);
+    public function demo(
+        LiveDemoRepository $liveDemoRepository,
+        string $demo,
+    ): Response {
+        $demo = $liveDemoRepository->find($demo);
+
+        return $this->render($demo->getTemplate(), ['demo' => $demo]);
     }
 }
diff --git a/ux.symfony.com/src/Controller/Demo/LiveMemoryController.php b/ux.symfony.com/src/Controller/Demo/LiveMemoryController.php
index 79938c9c..0aa59aa2 100644
--- a/ux.symfony.com/src/Controller/Demo/LiveMemoryController.php
+++ b/ux.symfony.com/src/Controller/Demo/LiveMemoryController.php
@@ -21,6 +21,7 @@ use Symfony\Component\Routing\Attribute\Route;
 class LiveMemoryController extends AbstractController
 {
     #[Route('/', name: 'app_demo_live_memory')]
+    #[Route('/', name: 'app_demo_live_component_live_memory')]
     public function __invoke(LiveDemoRepository $liveDemoRepository): Response
     {
         return $this->render('demos/live_memory/index.html.twig', [
@@ -33,6 +34,7 @@ class LiveMemoryController extends AbstractController
      * from the session. This can be useful when you want to start over.
      */
     #[Route('/reset', name: 'app_demo_live_memory_reset', env: 'dev')]
+    #[Route('/reset', name: 'app_demo_live_component_live_memory_reset', env: 'dev')]
     public function reset(GameStorageInterface $gameStorage): Response
     {
         $gameStorage->saveGame(null);
diff --git a/ux.symfony.com/src/Model/Demo.php b/ux.symfony.com/src/Model/Demo.php
new file mode 100644
index 00000000..9bcede51
--- /dev/null
+++ b/ux.symfony.com/src/Model/Demo.php
@@ -0,0 +1,58 @@
+<?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 App\Model;
+
+class Demo
+{
+    public function __construct(
+        private string $identifier,
+        private string $name,
+        private string $description,
+        private Person|string $author,
+        private \DateTimeImmutable|string $publishedAt,
+        private array $tags,
+    ) {
+    }
+
+    public function getIdentifier(): string
+    {
+        return $this->identifier;
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function getDescription(): string
+    {
+        return $this->description;
+    }
+
+    /**
+     * @return list<string>
+     */
+    public function getTags(): array
+    {
+        return $this->tags;
+    }
+
+    public function getAuthor(): Person|string
+    {
+        return $this->author;
+    }
+
+    public function getPublishedAt(): \DateTimeImmutable|string
+    {
+        return $this->publishedAt;
+    }
+}
diff --git a/ux.symfony.com/src/Model/LiveDemo.php b/ux.symfony.com/src/Model/LiveDemo.php
index cb91b3ec..ccd28acd 100644
--- a/ux.symfony.com/src/Model/LiveDemo.php
+++ b/ux.symfony.com/src/Model/LiveDemo.php
@@ -11,53 +11,37 @@
 
 namespace App\Model;
 
-class LiveDemo
+final class LiveDemo extends Demo
 {
     public function __construct(
-        private string $identifier,
-        private string $name,
-        private string $description,
-        private string $route,
+        string $identifier,
+        string $name,
+        string $description,
+        string $author,
+        string $publishedAt,
+        array $tags,
         private string $longDescription,
-        /**
-         * @var string[]
-         */
-        private array $tags = [],
     ) {
+        parent::__construct($identifier, $name, $description, $author, $publishedAt, $tags);
     }
 
-    public function getIdentifier(): string
+    public function getRoute(): string
     {
-        return $this->identifier;
+        return 'app_demo_live_component_'.str_replace('-', '_', parent::getIdentifier());
     }
 
-    public function getName(): string
+    public function getScreenshotFilename(?string $format = null): string
     {
-        return $this->name;
+        return 'images/live_demo/'.parent::getIdentifier().($format ? ('-'.$format) : '').'.png';
     }
 
-    public function getDescription(): string
+    public function getTemplate(): string
     {
-        return $this->description;
+        return \sprintf('demos/live_component/%s.html.twig', str_replace('-', '_', parent::getIdentifier()));
     }
 
     public function getLongDescription(): string
     {
         return $this->longDescription;
     }
-
-    public function getRoute(): string
-    {
-        return $this->route;
-    }
-
-    public function getTags(): array
-    {
-        return $this->tags;
-    }
-
-    public function getScreenshotFilename(?string $format = null): string
-    {
-        return 'images/live_demo/'.$this->identifier.($format ? ('-'.$format) : '').'.png';
-    }
 }
diff --git a/ux.symfony.com/src/Service/LiveDemoRepository.php b/ux.symfony.com/src/Service/LiveDemoRepository.php
index df459b2d..c65a6a16 100644
--- a/ux.symfony.com/src/Service/LiveDemoRepository.php
+++ b/ux.symfony.com/src/Service/LiveDemoRepository.php
@@ -25,141 +25,145 @@ class LiveDemoRepository
                 'infinite-scroll-2',
                 name: 'Infinite Scroll - 2/2',
                 description: 'Loading on-scroll, flexible layout grid, colorfull loading animations and... more T-Shirts!',
-                route: 'app_demo_live_component_infinite_scroll_2',
+                author: 'smnandre',
+                publishedAt: '2024-06-07',
+                tags: ['grid', 'pagination', 'loading', 'scroll'],
                 longDescription: <<<EOF
-The second and final part of the <strong>Infinite Scroll Serie</strong>, with a new range of (lovely) T-Shirts!
-<br>
-Now with <code>automatic loading on scroll</code>, a new trick and amazing <code>loading animations</code>!
+The second and final part of the **Infinite Scroll Serie**, with a new range of (lovely) T-Shirts!
+Now with `automatic loading on scroll`, a new trick and amazing `loading animations`!
 EOF,
-                tags: ['grid', 'pagination', 'loading', 'scroll'],
             ),
             new LiveDemo(
                 'infinite-scroll',
                 name: 'Infinite Scroll - 1/2',
                 description: 'Load more items as you scroll down the page.',
-                route: 'app_demo_live_component_infinite_scroll',
+                author: 'smnandre',
+                publishedAt: '2024-06-07',
+                tags: ['grid', 'pagination', 'navigation'],
                 longDescription: <<<EOF
 Infinite scroll allows users to continuously load content as they scroll down the page.
-<br><code>Part One</code> of this demo shows how to <code>append new items</code> to the page with a <a href="/live-component"><code>LiveComponent</code></a>.
+`Part One` of this demo shows how to `append new items` to the page with a [`LiveComponent`](/live-component).
 EOF,
-                tags: ['grid', 'pagination', 'navigation'],
             ),
             new LiveDemo(
                 'live-memory',
                 name: 'Live Memory Card Game',
                 description: 'A Memorable Game UX with Live Components!',
-                route: 'app_demo_live_memory',
+                author: 'smnandre',
+                publishedAt: '2024-06-07',
+                tags: ['game', 'time', 'events', 'LiveAction'],
                 longDescription: <<<EOF
 A Memorable Game UX with Live Components! Discover how to use Live Components to create a game with a vibrant interface,
  rich interactions and real-time updates. This journey will take you through many features of Live Components, and you'll
   learn how to use them to create a fun and engaging game.
 EOF,
-                tags: ['game', 'time', 'events', 'LiveAction'],
             ),
             new LiveDemo(
                 'auto-validating-form',
                 name: 'Auto-Validating Form',
                 description: 'Create a form that validates each field in-real-time as the user enters data!',
-                route: 'app_demo_live_component_auto_validating_form',
+                author: 'weaverryan',
+                publishedAt: '2022-06-17',
+                tags: ['form', 'validation', 'inline'],
                 longDescription: <<<EOF
 Enter a bad email or leave the password empty, and see how the
 form validates in real time!
-<br>
-This renders a normal Symfony form but with extras added on top,
+
+This renders a normal `Symfony` form but with extras added on top,
 all generated from Symfony & Twig.
 EOF,
-                tags: ['form', 'validation', 'inline'],
             ),
             new LiveDemo(
                 'form-collection-type',
                 name: 'Embedded CollectionType Form',
                 description: 'Create embedded forms with functional "add" and "remove" buttons all in Twig.',
-                route: 'app_demo_live_component_form_collection_type',
+                author: 'weaverryan',
+                publishedAt: '2022-06-17',
+                tags: ['form', 'collection'],
                 longDescription: <<<EOF
-Unlock the potential of Symfony's <a href="https://symfony.com/doc/current/reference/forms/types/collection.html"><code>CollectionType</code></a> while
-writing zero JavaScript.
-<br>
+Unlock the potential of Symfony's [`CollectionType`](https://symfony.com/doc/current/reference/forms/types/collection.html) while writing zero JavaScript.
 This demo shows off adding and removing items entirely in PHP & Twig.
 EOF,
-                tags: ['form', 'collection'],
             ),
             new LiveDemo(
                 'dependent-form-fields',
                 name: 'Dependent Form Fields',
                 description: 'After selecting the first field, automatically reload the options for a second field.',
-                route: 'app_demo_live_component_dependent_form_fields',
+                author: 'weaverryan',
+                publishedAt: '2022-06-17',
+                tags: ['form', 'field', 'events'],
                 longDescription: <<<EOF
-Unleash the power of form events, thanks to <a href="/live-component"><code>LiveComponent</code></a>
-and <a href="https://github.com/SymfonyCasts/dynamic-forms"><code>DynamicForms</code></a>.
+Unleash the power of form events, thanks to [`LiveComponent`](/live-component) and [`DynamicForms`](https://github.com/SymfonyCasts/dynamic-forms).
 EOF,
-                tags: ['form', 'field', 'events'],
             ),
             new LiveDemo(
                 'voting',
                 name: 'Up & Down Voting',
                 description: 'Save up & down votes live in pure Twig & PHP.',
-                route: 'app_demo_live_component_voting',
+                author: 'weaverryan',
+                publishedAt: '2022-06-17',
+                tags: ['form', 'LiveAction'],
                 longDescription: <<<EOF
 With each row as its own component, it's easy to add up & down voting + keep track of which items have been voted on.
-<br>
-This uses a <a href="https://symfony.com/bundles/ux-live-component/current/index.html#actions">LiveAction</a> to save everything with Ajax.
+This uses a [LiveAction](https://symfony.com/bundles/ux-live-component/current/index.html#actions) to save everything with Ajax.
 EOF,
-                tags: ['form', 'LiveAction'],
             ),
             new LiveDemo(
                 'inline-edit',
                 name: 'Inline Editing',
                 description: 'Activate an inline editing form with real-time validation.',
-                route: 'app_demo_live_component_inline_edit',
+                author: 'weaverryan',
+                publishedAt: '2023-02-21',
+                tags: ['form', 'inline', 'LiveAction'],
                 longDescription: <<<EOF
 Inline editing? Simple. Use LiveComponents to track if you're in "edit" mode, let
-the user update any fields on your entity, and save through a <code>LiveAction</code>.
+the user update any fields on your entity, and save through a `LiveAction`.
 EOF,
-                tags: ['form', 'inline', 'LiveAction'],
             ),
             new LiveDemo(
                 'chartjs',
                 name: 'Auto-Updating Chart',
                 description: 'Render & Update a Chart.js chart in real-time.',
-                route: 'app_demo_live_component_chartjs',
+                author: 'weaverryan',
+                publishedAt: '2023-03-16',
+                tags: ['chart', 'data', 'LiveAction', 'stimulus'],
                 longDescription: <<<EOF
 What do you get with Live Components + UX Chart.js + UX Autocomplete?
-<br>
+
 An auto-updating chart that you will ❤️.
 EOF,
-                tags: ['chart', 'data', 'LiveAction', 'stimulus'],
             ),
             new LiveDemo(
                 'invoice',
                 name: 'Invoice Creator',
                 description: 'Create an invoice + line items that updates as you type.',
-                route: 'app_demo_live_component_invoice',
+                author: 'weaverryan',
+                publishedAt: '2023-04-20',
+                tags: ['form', 'entity', 'events', 'LiveAction'],
                 longDescription: <<<EOF
 Create or edit an `Invoice` entity along with child components for each related `InvoiceItem` entity.
-<br>
+
 Children components emit events to communicate to the parent and everything is saved in a `saveInvoice` LiveAction method.
 EOF,
-                tags: ['form', 'entity', 'events', 'LiveAction'],
             ),
             new LiveDemo(
                 'product-form',
                 name: 'Product Form + Category Modal',
                 description: 'Create a Category on the fly - from inside a product form - via a modal.',
-                route: 'app_demo_live_component_product_form',
-                longDescription: <<<EOF
-Open a child modal component to create a new Category.
-EOF,
+                author: 'weaverryan',
+                publishedAt: '2023-04-20',
                 tags: ['form', 'entity', 'events', 'modal'],
+                longDescription: 'Open a child modal component to create a new Category.',
             ),
             new LiveDemo(
                 'upload',
                 name: 'Uploading files',
                 description: 'Upload file from your live component through a LiveAction.',
-                route: 'app_demo_live_component_upload',
-                longDescription: <<<EOF
-File uploads are tricky. Submit them to a `#[LiveAction]` with the `files` modifier on `data-live-action` then process them.
-EOF,
+                author: 'lustmored',
+                publishedAt: '2023-06-26',
                 tags: ['form', 'file', 'upload', 'LiveAction'],
+                longDescription: 'File uploads are tricky. Submit them to a `#[LiveAction]` with the `files` modifier
+                on `data-live-action` then process them.',
             ),
         ];
     }
diff --git a/ux.symfony.com/src/Twig/Components/Badge/GithubBadge.php b/ux.symfony.com/src/Twig/Components/Badge/GithubBadge.php
new file mode 100644
index 00000000..1dbd8732
--- /dev/null
+++ b/ux.symfony.com/src/Twig/Components/Badge/GithubBadge.php
@@ -0,0 +1,34 @@
+<?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 App\Twig\Components\Badge;
+
+use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
+
+#[AsTwigComponent(
+    name: 'Badge:Github',
+    template: 'components/Badge.html.twig',
+    exposePublicProps: false,
+)]
+final class GithubBadge extends Badge
+{
+    public string $username;
+
+    public string $label = 'Github';
+
+    public string $icon = 'github';
+
+    public function mount(string $username): void
+    {
+        $this->value = $username;
+        $this->url = 'https://github.com/'.$username;
+    }
+}
diff --git a/ux.symfony.com/src/Twig/Components/Badge/PackageBadge.php b/ux.symfony.com/src/Twig/Components/Badge/PackageBadge.php
new file mode 100644
index 00000000..1c1b2b5e
--- /dev/null
+++ b/ux.symfony.com/src/Twig/Components/Badge/PackageBadge.php
@@ -0,0 +1,31 @@
+<?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 App\Twig\Components\Badge;
+
+use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
+
+#[AsTwigComponent(
+    name: 'Badge:Package',
+    template: 'components/Badge.html.twig',
+    exposePublicProps: false,
+)]
+final class PackageBadge extends Badge
+{
+    public string $label = 'UX';
+
+    public string $icon = 'lucide:box';
+
+    public function mount(string $package): void
+    {
+        $this->value = $package;
+    }
+}
diff --git a/ux.symfony.com/templates/components/UploadFiles.html.twig b/ux.symfony.com/templates/components/UploadFiles.html.twig
index b9400d12..b90eaffc 100644
--- a/ux.symfony.com/templates/components/UploadFiles.html.twig
+++ b/ux.symfony.com/templates/components/UploadFiles.html.twig
@@ -22,7 +22,7 @@
     </div>
     <div class="form-group mb-3">
         <label for="multiple-files" class="form-label">Multiple files:</label>
-        <input type="file" name="multiple[]" autocomplete="off" multiple="" id="multiple-files" class="form-control-file">
+        <input type="file" class="form-control-file" name="multiple[]" autocomplete="off" multiple="" id="multiple-files" >
         <div class="form-text text-muted">Current files:
         {% if multipleUploadFilenames %}
         <ul>
diff --git a/ux.symfony.com/templates/cookbook/show.html.twig b/ux.symfony.com/templates/cookbook/show.html.twig
index f65fa0e6..45337389 100644
--- a/ux.symfony.com/templates/cookbook/show.html.twig
+++ b/ux.symfony.com/templates/cookbook/show.html.twig
@@ -50,17 +50,13 @@
             </div>
         </div>
 
-        <div class="container container-xl x-4 pt-4 px-md-5 pt-md-5">
-            <div class="content"  style="max-width: 75ch;margin-inline: auto;font-size: 1.25rem;line-height: 1.75;font-weight: 300;">
-                {{ cookbook.content|raw|markdown_to_html }}
-            </div>
+        <div class="content"  style="max-width: 75ch;margin-inline: auto;font-size: 1.25rem;line-height: 1.75;font-weight: 300;">
+            {{ cookbook.content|raw|markdown_to_html }}
         </div>
 
-        <div class="container container-xl x-4 pt-4 px-md-5 pt-md-5">
-            <div class="cluster">
-                <twig:Badge:Author author="{{ cookbook.author }}" />
-                <twig:Badge:Date label="Published" date="{{ cookbook.publishedAt }}" />
-            </div>
+        <div class="cluster">
+            <twig:Badge:Author author="{{ cookbook.author }}" />
+            <twig:Badge:Date label="Published" date="{{ cookbook.publishedAt }}" />
         </div>
 
     </article>
diff --git a/ux.symfony.com/templates/demos/live_demo.html.twig b/ux.symfony.com/templates/demos/live_demo.html.twig
index 75dac84e..cbb41700 100644
--- a/ux.symfony.com/templates/demos/live_demo.html.twig
+++ b/ux.symfony.com/templates/demos/live_demo.html.twig
@@ -7,7 +7,7 @@
     canonical: url(demo.route),
     social: {
         title: demo.name ~ ' - Symfony UX',
-        description: demo.longDescription|striptags,
+        description: demo.longDescription|markdown_to_html|striptags,
         image: {
             url: absolute_url(asset(demo.screenshotFilename('1280x720'))),
             type: 'image/png',
@@ -71,18 +71,12 @@
         </div>
     {% endblock %}
 
-{# {% block demo_tags %} #}
-{# <div class="container-fluid container-xxl px-4 px-md-5"> #}
-{# <div class="cluster"> #}
-{# <twig:Badge #}
-{# icon="lucide:box" #}
-{# label="Package" #}
-{# value="Live Component" #}
-{# url="{{ url('app_live_component') }}" #}
-{# /> #}
-{# </div> #}
-{# </div> #}
-{# {% endblock %} #}
+    <div class="container container-xl x-4 pt-4 px-md-5 pt-md-5">
+        <div class="cluster">
+            <twig:Badge:Github label="Author" username="{{ demo.author }}" />
+            <twig:Badge:Date label="Published" date="{{ demo.publishedAt }}" />
+        </div>
+    </div>
 
 {% endblock %}
 
diff --git a/ux.symfony.com/templates/main/homepage.html.twig b/ux.symfony.com/templates/main/homepage.html.twig
index 2de3ad10..451a647a 100644
--- a/ux.symfony.com/templates/main/homepage.html.twig
+++ b/ux.symfony.com/templates/main/homepage.html.twig
@@ -105,15 +105,21 @@
 
     <p class="eyebrows" style="margin: 111px 0 0;">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">Install extra <em class="rainbow-emphasis">Packages</em></h2>
+        <h2 class="ubuntu pt-2 component-headlines">Install UX <em class="rainbow-emphasis">Components</em></h2>
+
+{# <a class="Button Button--dark" href="{{ url('app_packages') }}"> #}
+{# Browse all Packages #}
+{# <twig:ux:icon name="arrow-right" style="transform: rotate(-45deg);"/> #}
+{# </a> #}
+
         <a style="height: 100%;" class="btn btn-md btn-outline-primary mt-3 mt-md-0"  href="{{ path('app_packages') }}">
             Browse all Packages
             <twig:ux:icon name="arrow-right" style="transform: rotate(-45deg);"/>
         </a>
     </div>
     <div class="row mt-3 mt-md-5">
-        <div style="display: grid; gap: 3rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));">
-            {% for package in packages %}
+        <div style="display: grid; gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));">
+            {% for package in packages|slice(0, 12) %}
                 <twig:Package:PackageBox package="{{ package }}" titleTag="h3" />
             {% endfor %}
         </div>
diff --git a/ux.symfony.com/templates/ux_packages/map.html.twig b/ux.symfony.com/templates/ux_packages/map.html.twig
index 8f7afe5e..0277e6cd 100644
--- a/ux.symfony.com/templates/ux_packages/map.html.twig
+++ b/ux.symfony.com/templates/ux_packages/map.html.twig
@@ -34,7 +34,7 @@
 {% block demo_content %}
     <div>
         {# The map must have a defined height #}
-        {{ ux_map(map, {style: 'height: 600px;'}) }}
+        {{ render_map(map, {style: 'height: 600px;'}) }}
     </div>
 {% endblock %}
 


Test patch:
diff --git a/src/Chartjs/tests/Twig/ChartExtensionTest.php b/src/Chartjs/tests/Twig/ChartExtensionTest.php
index 0f3ae34e..03472690 100644
--- a/src/Chartjs/tests/Twig/ChartExtensionTest.php
+++ b/src/Chartjs/tests/Twig/ChartExtensionTest.php
@@ -56,7 +56,7 @@ class ChartExtensionTest extends TestCase
         );
 
         $this->assertSame(
-            '<canvas data-controller="mycontroller symfony--ux-chartjs--chart" data-symfony--ux-chartjs--chart-view-value="{&quot;type&quot;:&quot;line&quot;,&quot;data&quot;:{&quot;labels&quot;:[&quot;January&quot;,&quot;February&quot;,&quot;March&quot;,&quot;April&quot;,&quot;May&quot;,&quot;June&quot;,&quot;July&quot;],&quot;datasets&quot;:[{&quot;label&quot;:&quot;My First dataset&quot;,&quot;backgroundColor&quot;:&quot;rgb(255, 99, 132)&quot;,&quot;borderColor&quot;:&quot;rgb(255, 99, 132)&quot;,&quot;data&quot;:[0,10,5,2,20,30,45]}]},&quot;options&quot;:{&quot;showLines&quot;:false}}" class="myclass"></canvas>',
+            '<canvas data-controller="mycontroller symfony--ux-chartjs--chart" data-symfony--ux-chartjs--chart-view-value="&#x7B;&quot;type&quot;&#x3A;&quot;line&quot;,&quot;data&quot;&#x3A;&#x7B;&quot;labels&quot;&#x3A;&#x5B;&quot;January&quot;,&quot;February&quot;,&quot;March&quot;,&quot;April&quot;,&quot;May&quot;,&quot;June&quot;,&quot;July&quot;&#x5D;,&quot;datasets&quot;&#x3A;&#x5B;&#x7B;&quot;label&quot;&#x3A;&quot;My&#x20;First&#x20;dataset&quot;,&quot;backgroundColor&quot;&#x3A;&quot;rgb&#x28;255,&#x20;99,&#x20;132&#x29;&quot;,&quot;borderColor&quot;&#x3A;&quot;rgb&#x28;255,&#x20;99,&#x20;132&#x29;&quot;,&quot;data&quot;&#x3A;&#x5B;0,10,5,2,20,30,45&#x5D;&#x7D;&#x5D;&#x7D;,&quot;options&quot;&#x3A;&#x7B;&quot;showLines&quot;&#x3A;false&#x7D;&#x7D;" class="myclass"></canvas>',
             $rendered
         );
     }
diff --git a/src/LiveComponent/tests/Unit/Twig/LiveComponentRuntimeTest.php b/src/LiveComponent/tests/Unit/Twig/LiveComponentRuntimeTest.php
index 01f067c0..30c3ba8c 100644
--- a/src/LiveComponent/tests/Unit/Twig/LiveComponentRuntimeTest.php
+++ b/src/LiveComponent/tests/Unit/Twig/LiveComponentRuntimeTest.php
@@ -28,15 +28,17 @@ final class LiveComponentRuntimeTest extends KernelTestCase
         $this->assertSame('data-action="live#action" data-live-prop1-param="val1" data-live-some-prop-param="val2" data-live-action-param="action-name"', $props);
 
         $props = $runtime->liveAction('action-name', ['prop1' => 'val1', 'prop2' => 'val2'], ['debounce' => 300]);
-        $this->assertSame('data-action="live#action" data-live-prop1-param="val1" data-live-prop2-param="val2" data-live-action-param="debounce(300)|action-name"', $props);
+        $this->assertSame('data-action="live#action" data-live-prop1-param="val1" data-live-prop2-param="val2" data-live-action-param="debounce(300)|action-name"', html_entity_decode($props));
+        $this->assertSame('data-action="live#action" data-live-prop1-param="val1" data-live-prop2-param="val2" data-live-action-param="debounce&#x28;300&#x29;&#x7C;action-name"', $props);
 
         $props = $runtime->liveAction('action-name:prevent', ['pro1' => 'val1', 'prop2' => 'val2'], ['debounce' => 300]);
-        $this->assertSame('data-action="live#action:prevent" data-live-pro1-param="val1" data-live-prop2-param="val2" data-live-action-param="debounce(300)|action-name"', $props);
+        $this->assertSame('data-action="live#action:prevent" data-live-pro1-param="val1" data-live-prop2-param="val2" data-live-action-param="debounce(300)|action-name"', html_entity_decode($props));
+        $this->assertSame('data-action="live#action&#x3A;prevent" data-live-pro1-param="val1" data-live-prop2-param="val2" data-live-action-param="debounce&#x28;300&#x29;&#x7C;action-name"', $props);
 
         $props = $runtime->liveAction('action-name:prevent', [], ['debounce' => 300]);
-        $this->assertSame('data-action="live#action:prevent" data-live-action-param="debounce(300)|action-name"', $props);
+        $this->assertSame('data-action="live#action:prevent" data-live-action-param="debounce(300)|action-name"', html_entity_decode($props));
 
         $props = $runtime->liveAction('action-name', [], [], 'keydown.esc');
-        $this->assertSame('data-action="keydown.esc->live#action" data-live-action-param="action-name"', $props);
+        $this->assertSame('data-action="keydown.esc->live#action" data-live-action-param="action-name"', html_entity_decode($props));
     }
 }
diff --git a/src/Map/assets/test/abstract_map_controller.test.ts b/src/Map/assets/test/abstract_map_controller.test.ts
index c9e0e38a..0beadef2 100644
--- a/src/Map/assets/test/abstract_map_controller.test.ts
+++ b/src/Map/assets/test/abstract_map_controller.test.ts
@@ -20,28 +20,14 @@ class MyMapController extends AbstractMapController {
         const marker = { marker: 'marker', title: definition.title };
 
         if (definition.infoWindow) {
-            this.createInfoWindow({ definition: definition.infoWindow, element: marker });
+            this.createInfoWindow({ definition: definition.infoWindow, marker });
         }
 
         return marker;
     }
 
-    doCreatePolygon(definition) {
-        const polygon = { polygon: 'polygon', title: definition.title };
-
-        if (definition.infoWindow) {
-            this.createInfoWindow({ definition: definition.infoWindow, element: polygon });
-        }
-        return polygon;
-    }
-
-    doCreateInfoWindow({ definition, element }) {
-        if (element.marker) {
-            return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: element.title };
-        }
-        if (element.polygon) {
-            return { infoWindow: 'infoWindow', headerContent: definition.headerContent, polygon: element.title };
-        }
+    doCreateInfoWindow({ definition, marker }) {
+        return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: marker.title };
     }
 
     doFitBoundsToMarkers() {
@@ -61,61 +47,12 @@ describe('AbstractMapController', () => {
     beforeEach(() => {
         container = mountDOM(`
           <div 
-            data-testid="map" 
-            data-controller="map" 
-            style="height: 700px; margin: 10px;" 
-            data-map-provider-options-value="{}" 
-            data-map-view-value='{
-                "center": { "lat": 48.8566, "lng": 2.3522 },
-                "zoom": 4,
-                "fitBoundsToMarkers": true,
-                "options": {},
-                "markers": [
-                    {
-                        "position": { "lat": 48.8566, "lng": 2.3522 },
-                        "title": "Paris",
-                        "infoWindow": null
-                    },
-                    {
-                        "position": { "lat": 45.764, "lng": 4.8357 },
-                        "title": "Lyon",
-                        "infoWindow": {
-                            "headerContent": "<b>Lyon</b>",
-                            "content": "The French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.",
-                            "position": null,
-                            "opened": false,
-                            "autoClose": true
-                        }
-                    }
-                ],
-                "polygons": [
-                    {
-                        "coordinates": [
-                            { "lat": 48.858844, "lng": 2.294351 },
-                            { "lat": 48.853, "lng": 2.3499 },
-                            { "lat": 48.8566, "lng": 2.3522 }
-                        ],
-                        "title": "Polygon 1",
-                        "infoWindow": null
-                    },
-                    {
-                        "coordinates": [
-                            { "lat": 45.764043, "lng": 4.835659 },
-                            { "lat": 45.750000, "lng": 4.850000 },
-                            { "lat": 45.770000, "lng": 4.820000 }
-                        ],
-                        "title": "Polygon 2",
-                        "infoWindow": {
-                            "headerContent": "<b>Polygon 2</b>",
-                            "content": "A polygon around Lyon with some additional info.",
-                            "position": null,
-                            "opened": false,
-                            "autoClose": true
-                        }
-                    }
-                ]
-            }'>
-        </div>
+              data-testid="map" 
+              data-controller="map" 
+              style="height&#x3A;&#x20;700px&#x3B;&#x20;margin&#x3A;&#x20;10px" 
+              data-map-provider-options-value="&#x7B;&#x7D;" 
+              data-map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"
+          ></div>
         `);
     });
 
@@ -123,7 +60,7 @@ describe('AbstractMapController', () => {
         clearDOM();
     });
 
-    it('connect and create map, marker, polygon and info window', async () => {
+    it('connect and create map, marker and info window', async () => {
         const div = getByTestId(container, 'map');
         expect(div).not.toHaveClass('connected');
 
@@ -136,21 +73,12 @@ describe('AbstractMapController', () => {
             { marker: 'marker', title: 'Paris' },
             { marker: 'marker', title: 'Lyon' },
         ]);
-        expect(controller.polygons).toEqual([
-            { polygon: 'polygon', title: 'Polygon 1' },
-            { polygon: 'polygon', title: 'Polygon 2' },
-        ]);
         expect(controller.infoWindows).toEqual([
             {
                 headerContent: '<b>Lyon</b>',
                 infoWindow: 'infoWindow',
                 marker: 'Lyon',
             },
-            {
-                headerContent: '<b>Polygon 2</b>',
-                infoWindow: 'infoWindow',
-                polygon: 'Polygon 2',
-            },
         ]);
     });
 });
diff --git a/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts b/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts
index f1b08abb..1db8edff 100644
--- a/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts
+++ b/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts
@@ -41,7 +41,7 @@ describe('GoogleMapsController', () => {
               data-controller="check google" 
               style="height&#x3A;&#x20;700px&#x3B;&#x20;margin&#x3A;&#x20;10px" 
               data-google-provider-options-value="&#x7B;&quot;version&quot;&#x3A;&quot;weekly&quot;,&quot;libraries&quot;&#x3A;&#x5B;&quot;maps&quot;,&quot;marker&quot;&#x5D;,&quot;apiKey&quot;&#x3A;&quot;&quot;&#x7D;" 
-              data-google-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;&quot;YOUR_MAP_ID&quot;,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControl&quot;&#x3A;true,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControl&quot;&#x3A;true,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControl&quot;&#x3A;true,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControl&quot;&#x3A;true,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;,&quot;polygons&quot;&#x3A;[]&#x7D;"
+              data-google-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;&quot;YOUR_MAP_ID&quot;,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControl&quot;&#x3A;true,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControl&quot;&#x3A;true,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControl&quot;&#x3A;true,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControl&quot;&#x3A;true,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"
           ></div>
         `);
     });
diff --git a/src/Map/src/Bridge/Google/assets/vitest.config.mjs b/src/Map/src/Bridge/Google/assets/vitest.config.mjs
index c957798d..dde4635d 100644
--- a/src/Map/src/Bridge/Google/assets/vitest.config.mjs
+++ b/src/Map/src/Bridge/Google/assets/vitest.config.mjs
@@ -6,7 +6,7 @@ export default mergeConfig(
     defineConfig({
         resolve: {
             alias: {
-                '@symfony/ux-map': __dirname + '/../../../../assets/src/abstract_map_controller.ts',
+                '@symfony/ux-map/abstract-map-controller': __dirname + '/../../../../assets/src/abstract_map_controller.ts',
             },
         },
         define: {
diff --git a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
index e688d0c8..db011e30 100644
--- a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
+++ b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
@@ -29,26 +29,26 @@ class GoogleRendererTest extends RendererTestCase
             ->zoom(12);
 
         yield 'simple map, with minimum options' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-view-value="{&quot;center&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;zoom&quot;:12,&quot;fitBoundsToMarkers&quot;:false,&quot;options&quot;:{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20}},&quot;markers&quot;:[],&quot;polygons&quot;:[]}"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => $map,
         ];
 
         yield 'with every options' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;id&quot;:&quot;gmap&quot;,&quot;language&quot;:&quot;fr&quot;,&quot;region&quot;:&quot;FR&quot;,&quot;nonce&quot;:&quot;abcd&quot;,&quot;retries&quot;:10,&quot;url&quot;:&quot;https:\/\/maps.googleapis.com\/maps\/api\/js&quot;,&quot;version&quot;:&quot;quarterly&quot;,&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-view-value="{&quot;center&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;zoom&quot;:12,&quot;fitBoundsToMarkers&quot;:false,&quot;options&quot;:{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20}},&quot;markers&quot;:[],&quot;polygons&quot;:[]}"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;id&quot;&#x3A;&quot;gmap&quot;,&quot;language&quot;&#x3A;&quot;fr&quot;,&quot;region&quot;&#x3A;&quot;FR&quot;,&quot;nonce&quot;&#x3A;&quot;abcd&quot;,&quot;retries&quot;&#x3A;10,&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;maps.googleapis.com&#x5C;&#x2F;maps&#x5C;&#x2F;api&#x5C;&#x2F;js&quot;,&quot;version&quot;&#x3A;&quot;quarterly&quot;,&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key', id: 'gmap', language: 'fr', region: 'FR', nonce: 'abcd', retries: 10, url: 'https://maps.googleapis.com/maps/api/js', version: 'quarterly'),
             'map' => $map,
         ];
 
         yield 'with custom attributes' => [
-            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-view-value="{&quot;center&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;zoom&quot;:12,&quot;fitBoundsToMarkers&quot;:false,&quot;options&quot;:{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20}},&quot;markers&quot;:[],&quot;polygons&quot;:[]}" class="map"></div>',
+            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;" class="map"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => $map,
             'attributes' => ['data-controller' => 'my-custom-controller', 'class' => 'map'],
         ];
 
         yield 'with markers and infoWindows' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-view-value="{&quot;center&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;zoom&quot;:12,&quot;fitBoundsToMarkers&quot;:false,&quot;options&quot;:{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20}},&quot;markers&quot;:[{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Paris&quot;,&quot;infoWindow&quot;:null,&quot;extra&quot;:{}},{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Lyon&quot;,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Lyon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:{}},&quot;extra&quot;:{}}],&quot;polygons&quot;:[]}"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;null,&quot;content&quot;&#x3A;&quot;Lyon&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x5D;&#x7D;"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => (clone $map)
                 ->addMarker(new Marker(new Point(48.8566, 2.3522), 'Paris'))
@@ -56,7 +56,7 @@ class GoogleRendererTest extends RendererTestCase
         ];
 
         yield 'with controls enabled' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-view-value="{&quot;center&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;zoom&quot;:12,&quot;fitBoundsToMarkers&quot;:false,&quot;options&quot;:{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20}},&quot;markers&quot;:[],&quot;polygons&quot;:[]}"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => (clone $map)
                 ->options(new GoogleOptions(
@@ -68,7 +68,7 @@ class GoogleRendererTest extends RendererTestCase
         ];
 
         yield 'without controls enabled' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-view-value="{&quot;center&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;zoom&quot;:12,&quot;fitBoundsToMarkers&quot;:false,&quot;options&quot;:{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false},&quot;markers&quot;:[],&quot;polygons&quot;:[]}"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => (clone $map)
                 ->options(new GoogleOptions(
diff --git a/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts b/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts
index 5a51bf5f..e6aa9276 100644
--- a/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts
@@ -36,12 +36,12 @@ describe('LeafletController', () => {
 
     beforeEach(() => {
         container = mountDOM(`
-           <div 
+          <div 
               data-testid="map" 
               data-controller="check leaflet" 
               style="height&#x3A;&#x20;700px&#x3B;&#x20;margin&#x3A;&#x20;10px" 
               data-leaflet-provider-options-value="&#x7B;&#x7D;" 
-              data-leaflet-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;,&quot;polygons&quot;&#x3A;[]&#x7D;"
+              data-leaflet-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"
           ></div>
         `);
     });
diff --git a/src/Map/src/Bridge/Leaflet/assets/vitest.config.mjs b/src/Map/src/Bridge/Leaflet/assets/vitest.config.mjs
index 8fb88b88..7072015e 100644
--- a/src/Map/src/Bridge/Leaflet/assets/vitest.config.mjs
+++ b/src/Map/src/Bridge/Leaflet/assets/vitest.config.mjs
@@ -6,7 +6,7 @@ export default mergeConfig(
     defineConfig({
         resolve: {
             alias: {
-                '@symfony/ux-map': __dirname + '/../../../../assets/src/abstract_map_controller.ts',
+                '@symfony/ux-map/abstract-map-controller': __dirname + '/../../../../assets/src/abstract_map_controller.ts',
                 'leaflet/dist/leaflet.min.css': 'leaflet/dist/leaflet.css',
             },
         },
diff --git a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
index a23f103e..6931f53a 100644
--- a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
+++ b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
@@ -28,20 +28,20 @@ class LeafletRendererTest extends RendererTestCase
             ->zoom(12);
 
         yield 'simple map' => [
-            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-view-value="{&quot;center&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;zoom&quot;:12,&quot;fitBoundsToMarkers&quot;:false,&quot;options&quot;:{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:{}}},&quot;markers&quot;:[],&quot;polygons&quot;:[]}"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new LeafletRenderer(new StimulusHelper(null)),
             'map' => $map,
         ];
 
         yield 'with custom attributes' => [
-            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-view-value="{&quot;center&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;zoom&quot;:12,&quot;fitBoundsToMarkers&quot;:false,&quot;options&quot;:{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:{}}},&quot;markers&quot;:[],&quot;polygons&quot;:[]}" class="map"></div>',
+            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;" class="map"></div>',
             'renderer' => new LeafletRenderer(new StimulusHelper(null)),
             'map' => $map,
             'attributes' => ['data-controller' => 'my-custom-controller', 'class' => 'map'],
         ];
 
         yield 'with markers and infoWindows' => [
-            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-view-value="{&quot;center&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;zoom&quot;:12,&quot;fitBoundsToMarkers&quot;:false,&quot;options&quot;:{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:{}}},&quot;markers&quot;:[{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Paris&quot;,&quot;infoWindow&quot;:null,&quot;extra&quot;:{}},{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Lyon&quot;,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Lyon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:{}},&quot;extra&quot;:{}}],&quot;polygons&quot;:[]}"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;null,&quot;content&quot;&#x3A;&quot;Lyon&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x5D;&#x7D;"></div>',
             'renderer' => new LeafletRenderer(new StimulusHelper(null)),
             'map' => (clone $map)
                 ->addMarker(new Marker(new Point(48.8566, 2.3522), 'Paris'))
diff --git a/src/Map/tests/MapFactoryTest.php b/src/Map/tests/MapFactoryTest.php
index fcff3b05..a19e2d79 100644
--- a/src/Map/tests/MapFactoryTest.php
+++ b/src/Map/tests/MapFactoryTest.php
@@ -32,13 +32,6 @@ class MapFactoryTest extends TestCase
         $this->assertSame($array['markers'][0]['title'], $markers[0]['title']);
         $this->assertSame($array['markers'][0]['infoWindow']['headerContent'], $markers[0]['infoWindow']['headerContent']);
         $this->assertSame($array['markers'][0]['infoWindow']['content'], $markers[0]['infoWindow']['content']);
-
-        $this->assertCount(1, $polygons = $map->toArray()['polygons']);
-        $this->assertEquals($array['polygons'][0]['points'], $polygons[0]['points']);
-        $this->assertEquals($array['polygons'][0]['points'], $polygons[0]['points']);
-        $this->assertSame($array['polygons'][0]['title'], $polygons[0]['title']);
-        $this->assertSame($array['polygons'][0]['infoWindow']['headerContent'], $polygons[0]['infoWindow']['headerContent']);
-        $this->assertSame($array['polygons'][0]['infoWindow']['content'], $polygons[0]['infoWindow']['content']);
     }
 
     public function testFromArrayWithInvalidCenter(): void
@@ -83,30 +76,6 @@ class MapFactoryTest extends TestCase
         Map::fromArray($array);
     }
 
-    public function testFromArrayWithInvalidPolygons(): void
-    {
-        $array = self::createMapArray();
-        $array['polygons'] = 'invalid';
-
-        $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionMessage('The "polygons" parameter must be an array.');
-        Map::fromArray($array);
-    }
-
-    public function testFromArrayWithInvalidPolygon(): void
-    {
-        $array = self::createMapArray();
-        $array['polygons'] = [
-            [
-                'invalid',
-            ],
-        ];
-
-        $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionMessage('The "points" parameter is required.');
-        Map::fromArray($array);
-    }
-
     private static function createMapArray(): array
     {
         return [
@@ -128,29 +97,6 @@ class MapFactoryTest extends TestCase
                     ],
                 ],
             ],
-            'polygons' => [
-                [
-                    'points' => [
-                        [
-                            'lat' => 48.858844,
-                            'lng' => 2.294351,
-                        ],
-                        [
-                            'lat' => 48.853,
-                            'lng' => 2.3499,
-                        ],
-                        [
-                            'lat' => 48.8566,
-                            'lng' => 2.3522,
-                        ],
-                    ],
-                    'title' => 'Polygon 1',
-                    'infoWindow' => [
-                        'headerContent' => 'Polygon 1',
-                        'content' => 'Polygon 1',
-                    ],
-                ],
-            ],
         ];
     }
 }
diff --git a/src/Map/tests/MapTest.php b/src/Map/tests/MapTest.php
index 95703724..99a4f1eb 100644
--- a/src/Map/tests/MapTest.php
+++ b/src/Map/tests/MapTest.php
@@ -18,7 +18,6 @@ use Symfony\UX\Map\Map;
 use Symfony\UX\Map\MapOptionsInterface;
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
-use Symfony\UX\Map\Polygon;
 
 class MapTest extends TestCase
 {
@@ -56,7 +55,6 @@ class MapTest extends TestCase
             'fitBoundsToMarkers' => true,
             'options' => $array['options'],
             'markers' => [],
-            'polygons' => [],
         ], $array);
     }
 
@@ -75,7 +73,6 @@ class MapTest extends TestCase
             'fitBoundsToMarkers' => false,
             'options' => $array['options'],
             'markers' => [],
-            'polygons' => [],
         ], $array);
     }
 
@@ -108,36 +105,11 @@ class MapTest extends TestCase
                 position: new Point(43.2965, 5.3698),
                 title: 'Marseille',
                 infoWindow: new InfoWindow(headerContent: '<b>Marseille</b>', content: 'Marseille', position: new Point(43.2965, 5.3698), opened: true)
-            ))
-            ->addPolygon(new Polygon(
-                points: [
-                    new Point(48.858844, 2.294351),
-                    new Point(48.853, 2.3499),
-                    new Point(48.8566, 2.3522),
-                ],
-                title: 'Polygon 1',
-                infoWindow: null,
-            ))
-            ->addPolygon(new Polygon(
-                points: [
-                    new Point(45.764043, 4.835659),
-                    new Point(45.75, 4.85),
-                    new Point(45.77, 4.82),
-                ],
-                title: 'Polygon 2',
-                infoWindow: new InfoWindow(
-                    headerContent: '<b>Polygon 2</b>',
-                    content: 'A polygon around Lyon with some additional info.',
-                    position: new Point(45.764, 4.8357),
-                    opened: true,
-                    autoClose: true,
-                ),
-            ))
-        ;
+            ));
 
         $array = $map->toArray();
 
-        self::assertEquals([
+        self::assertSame([
             'center' => ['lat' => 48.8566, 'lng' => 2.3522],
             'zoom' => 6.0,
             'fitBoundsToMarkers' => true,
@@ -183,35 +155,6 @@ class MapTest extends TestCase
                     'extra' => $array['markers'][2]['extra'],
                 ],
             ],
-            'polygons' => [
-                [
-                    'points' => [
-                        ['lat' => 48.858844, 'lng' => 2.294351],
-                        ['lat' => 48.853, 'lng' => 2.3499],
-                        ['lat' => 48.8566, 'lng' => 2.3522],
-                    ],
-                    'title' => 'Polygon 1',
-                    'infoWindow' => null,
-                    'extra' => $array['polygons'][0]['extra'],
-                ],
-                [
-                    'points' => [
-                        ['lat' => 45.764043, 'lng' => 4.835659],
-                        ['lat' => 45.75, 'lng' => 4.85],
-                        ['lat' => 45.77, 'lng' => 4.82],
-                    ],
-                    'title' => 'Polygon 2',
-                    'infoWindow' => [
-                        'headerContent' => '<b>Polygon 2</b>',
-                        'content' => 'A polygon around Lyon with some additional info.',
-                        'position' => ['lat' => 45.764, 'lng' => 4.8357],
-                        'opened' => true,
-                        'autoClose' => true,
-                        'extra' => $array['polygons'][1]['infoWindow']['extra'],
-                    ],
-                    'extra' => $array['polygons'][1]['extra'],
-                ],
-            ],
         ], $array);
 
         self::assertSame('roadmap', $array['options']->mapTypeId);
diff --git a/src/Notify/tests/Twig/NotifyRuntimeTest.php b/src/Notify/tests/Twig/NotifyRuntimeTest.php
index 8c3b3786..7f21970f 100644
--- a/src/Notify/tests/Twig/NotifyRuntimeTest.php
+++ b/src/Notify/tests/Twig/NotifyRuntimeTest.php
@@ -39,13 +39,13 @@ class NotifyRuntimeTest extends TestCase
 
     public static function streamNotificationsDataProvider(): iterable
     {
-        $publicUrl = 'http://localhost:9090/.well-known/mercure';
+        $publicUrl = 'http&#x3A;&#x2F;&#x2F;localhost&#x3A;9090&#x2F;.well-known&#x2F;mercure';
 
         yield [
             [['/topic/1', '/topic/2']],
             '<div '.
                 'data-controller="symfony--ux-notify--notify" '.
-                'data-symfony--ux-notify--notify-topics-value="[&quot;\/topic\/1&quot;,&quot;\/topic\/2&quot;]" '.
+                'data-symfony--ux-notify--notify-topics-value="&#x5B;&quot;&#x5C;&#x2F;topic&#x5C;&#x2F;1&quot;,&quot;&#x5C;&#x2F;topic&#x5C;&#x2F;2&quot;&#x5D;" '.
                 'data-symfony--ux-notify--notify-hub-value="'.$publicUrl.'"'.
             '></div>',
         ];
@@ -54,7 +54,7 @@ class NotifyRuntimeTest extends TestCase
             ['/topic/1'],
             '<div '.
                 'data-controller="symfony--ux-notify--notify" '.
-                'data-symfony--ux-notify--notify-topics-value="[&quot;\/topic\/1&quot;]" '.
+                'data-symfony--ux-notify--notify-topics-value="&#x5B;&quot;&#x5C;&#x2F;topic&#x5C;&#x2F;1&quot;&#x5D;" '.
                 'data-symfony--ux-notify--notify-hub-value="'.$publicUrl.'"'.
             '></div>',
         ];
@@ -63,7 +63,7 @@ class NotifyRuntimeTest extends TestCase
             [],
             '<div '.
                 'data-controller="symfony--ux-notify--notify" '.
-                'data-symfony--ux-notify--notify-topics-value="[&quot;https:\/\/symfony.com\/notifier&quot;]" '.
+                'data-symfony--ux-notify--notify-topics-value="&#x5B;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;symfony.com&#x5C;&#x2F;notifier&quot;&#x5D;" '.
                 'data-symfony--ux-notify--notify-hub-value="'.$publicUrl.'"'.
             '></div>',
         ];
diff --git a/src/React/tests/Twig/ReactComponentExtensionTest.php b/src/React/tests/Twig/ReactComponentExtensionTest.php
index 9fbf8c8b..aae2d6b6 100644
--- a/src/React/tests/Twig/ReactComponentExtensionTest.php
+++ b/src/React/tests/Twig/ReactComponentExtensionTest.php
@@ -36,7 +36,7 @@ class ReactComponentExtensionTest extends TestCase
         );
 
         $this->assertSame(
-            'data-controller="symfony--ux-react--react" data-symfony--ux-react--react-component-value="SubDir/MyComponent" data-symfony--ux-react--react-props-value="{&quot;fullName&quot;:&quot;Titouan Galopin&quot;}"',
+            'data-controller="symfony--ux-react--react" data-symfony--ux-react--react-component-value="SubDir&#x2F;MyComponent" data-symfony--ux-react--react-props-value="&#x7B;&quot;fullName&quot;&#x3A;&quot;Titouan&#x20;Galopin&quot;&#x7D;"',
             $rendered
         );
     }
@@ -52,7 +52,7 @@ class ReactComponentExtensionTest extends TestCase
         $rendered = $extension->renderReactComponent('SubDir/MyComponent');
 
         $this->assertSame(
-            'data-controller="symfony--ux-react--react" data-symfony--ux-react--react-component-value="SubDir/MyComponent"',
+            'data-controller="symfony--ux-react--react" data-symfony--ux-react--react-component-value="SubDir&#x2F;MyComponent"',
             $rendered
         );
     }
diff --git a/src/StimulusBundle/tests/Dto/StimulusAttributesTest.php b/src/StimulusBundle/tests/Dto/StimulusAttributesTest.php
index 17527a34..63393d99 100644
--- a/src/StimulusBundle/tests/Dto/StimulusAttributesTest.php
+++ b/src/StimulusBundle/tests/Dto/StimulusAttributesTest.php
@@ -145,7 +145,7 @@ final class StimulusAttributesTest extends TestCase
     public function testAddAttribute()
     {
         $this->stimulusAttributes->addAttribute('foo', 'bar baz');
-        $this->assertSame('foo="bar baz"', (string) $this->stimulusAttributes);
+        $this->assertSame('foo="bar&#x20;baz"', (string) $this->stimulusAttributes);
         $this->assertSame(['foo' => 'bar baz'], $this->stimulusAttributes->toArray());
     }
 }
diff --git a/src/StimulusBundle/tests/Twig/StimulusTwigExtensionTest.php b/src/StimulusBundle/tests/Twig/StimulusTwigExtensionTest.php
index 0e8cdbf8..41793ed5 100644
--- a/src/StimulusBundle/tests/Twig/StimulusTwigExtensionTest.php
+++ b/src/StimulusBundle/tests/Twig/StimulusTwigExtensionTest.php
@@ -135,8 +135,8 @@ final class StimulusTwigExtensionTest extends TestCase
         $extension = new StimulusTwigExtension(new StimulusHelper($this->twig));
         $dto = $extension->renderStimulusController('my-controller', ['myValue' => 'scalar-value']);
         $this->assertSame(
-            'data-controller="my-controller another-controller" data-my-controller-my-value-value="scalar-value" data-another-controller-another-value-value="scalar-value 2" data-another-controller-json-value-value="{&quot;key&quot;:&quot;Value with quotes &#039; and \&quot;.&quot;}"',
-            (string) $extension->appendStimulusController($dto, 'another-controller', ['another-value' => 'scalar-value 2', 'jsonValue' => json_encode(['key' => 'Value with quotes \' and ".'])]),
+            'data-controller="my-controller another-controller" data-my-controller-my-value-value="scalar-value" data-another-controller-another-value-value="scalar-value&#x20;2"',
+            (string) $extension->appendStimulusController($dto, 'another-controller', ['another-value' => 'scalar-value 2']),
         );
     }
 
diff --git a/src/Svelte/tests/Twig/SvelteComponentExtensionTest.php b/src/Svelte/tests/Twig/SvelteComponentExtensionTest.php
index 7a9a6d03..8231c028 100644
--- a/src/Svelte/tests/Twig/SvelteComponentExtensionTest.php
+++ b/src/Svelte/tests/Twig/SvelteComponentExtensionTest.php
@@ -37,7 +37,7 @@ class SvelteComponentExtensionTest extends TestCase
         );
 
         $this->assertSame(
-            'data-controller="symfony--ux-svelte--svelte" data-symfony--ux-svelte--svelte-component-value="SubDir/MyComponent" data-symfony--ux-svelte--svelte-props-value="{&quot;fullName&quot;:&quot;Titouan Galopin&quot;}"',
+            'data-controller="symfony--ux-svelte--svelte" data-symfony--ux-svelte--svelte-component-value="SubDir&#x2F;MyComponent" data-symfony--ux-svelte--svelte-props-value="&#x7B;&quot;fullName&quot;&#x3A;&quot;Titouan&#x20;Galopin&quot;&#x7D;"',
             $rendered
         );
     }
@@ -53,7 +53,7 @@ class SvelteComponentExtensionTest extends TestCase
         $rendered = $extension->renderSvelteComponent('SubDir/MyComponent');
 
         $this->assertSame(
-            'data-controller="symfony--ux-svelte--svelte" data-symfony--ux-svelte--svelte-component-value="SubDir/MyComponent"',
+            'data-controller="symfony--ux-svelte--svelte" data-symfony--ux-svelte--svelte-component-value="SubDir&#x2F;MyComponent"',
             $rendered
         );
     }
@@ -73,7 +73,7 @@ class SvelteComponentExtensionTest extends TestCase
         );
 
         $this->assertSame(
-            'data-controller="symfony--ux-svelte--svelte" data-symfony--ux-svelte--svelte-component-value="SubDir/MyComponent" data-symfony--ux-svelte--svelte-props-value="{&quot;fullName&quot;:&quot;Titouan Galopin&quot;}" data-symfony--ux-svelte--svelte-intro-value="true"',
+            'data-controller="symfony--ux-svelte--svelte" data-symfony--ux-svelte--svelte-component-value="SubDir&#x2F;MyComponent" data-symfony--ux-svelte--svelte-props-value="&#x7B;&quot;fullName&quot;&#x3A;&quot;Titouan&#x20;Galopin&quot;&#x7D;" data-symfony--ux-svelte--svelte-intro-value="true"',
             $rendered
         );
     }
diff --git a/src/Translator/assets/test/translator.test.ts b/src/Translator/assets/test/translator.test.ts
index 6df854a1..5a468044 100644
--- a/src/Translator/assets/test/translator.test.ts
+++ b/src/Translator/assets/test/translator.test.ts
@@ -4,7 +4,6 @@ import {
     type NoParametersType,
     setLocale,
     setLocaleFallbacks,
-    throwWhenNotFound,
     trans,
 } from '../src/translator';
 
@@ -12,7 +11,6 @@ describe('Translator', () => {
     beforeEach(() => {
         setLocale(null);
         setLocaleFallbacks({});
-        throwWhenNotFound(false);
         document.documentElement.lang = '';
         document.documentElement.removeAttribute('data-symfony-ux-translator-locale');
     });
@@ -389,40 +387,6 @@ describe('Translator', () => {
             );
         });
 
-        test('missing message should return the message id when `throwWhenNotFound` is false', () => {
-            throwWhenNotFound(false);
-            setLocale('fr');
-
-            const MESSAGE_IN_ANOTHER_DOMAIN: Message<{ security: { parameters: NoParametersType } }, 'en'> = {
-                id: 'Invalid credentials.',
-                translations: {
-                    messages: {
-                        en: 'Invalid credentials.',
-                    },
-                },
-            };
-
-            expect(trans(MESSAGE_IN_ANOTHER_DOMAIN)).toEqual('Invalid credentials.');
-        });
-
-        test('missing message should throw an error if `throwWhenNotFound` is true', () => {
-            throwWhenNotFound(true);
-            setLocale('fr');
-
-            const MESSAGE_IN_ANOTHER_DOMAIN: Message<{ security: { parameters: NoParametersType } }, 'en'> = {
-                id: 'Invalid credentials.',
-                translations: {
-                    messages: {
-                        en: 'Invalid credentials.',
-                    },
-                },
-            };
-
-            expect(() => {
-                trans(MESSAGE_IN_ANOTHER_DOMAIN);
-            }).toThrow(`No translation message found with id "Invalid credentials.".`);
-        });
-
         test('message from intl domain should be prioritized over its non-intl equivalent', () => {
             const MESSAGE: Message<
                 { 'messages+intl-icu': { parameters: NoParametersType }; messages: { parameters: NoParametersType } },
diff --git a/src/Translator/tests/TranslationsDumperTest.php b/src/Translator/tests/TranslationsDumperTest.php
index 10b13d90..c68a9fef 100644
--- a/src/Translator/tests/TranslationsDumperTest.php
+++ b/src/Translator/tests/TranslationsDumperTest.php
@@ -57,7 +57,6 @@ export const NOTIFICATION_COMMENT_CREATED = {"id":"notification.comment_created"
 export const NOTIFICATION_COMMENT_CREATED_DESCRIPTION = {"id":"notification.comment_created.description","translations":{"messages+intl-icu":{"en":"Your post \"{title}\" has received a new comment. You can read the comment by following <a href=\"{link}\">this link<\/a>","fr":"Votre article \"{title}\" a re\u00e7u un nouveau commentaire. Vous pouvez lire le commentaire en suivant <a href=\"{link}\">ce lien<\/a>"}}};
 export const POST_NUM_COMMENTS = {"id":"post.num_comments","translations":{"messages+intl-icu":{"en":"{count, plural, one {# comment} other {# comments}}","fr":"{count, plural, one {# commentaire} other {# commentaires}}"},"foobar":{"en":"There is 1 comment|There are %count% comments","fr":"Il y a 1 comment|Il y a %count% comments"}}};
 export const POST_NUM_COMMENTS_1 = {"id":"post.num_comments.","translations":{"messages+intl-icu":{"en":"{count, plural, one {# comment} other {# comments}} (should not conflict with the previous one.)","fr":"{count, plural, one {# commentaire} other {# commentaires}} (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}};
-export const POST_NUM_COMMENTS_2 = {"id":"post.num_comments..","translations":{"messages+intl-icu":{"en":"{count, plural, one {# comment} other {# comments}} (should not conflict with the previous one.)","fr":"{count, plural, one {# commentaire} other {# commentaires}} (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}};
 export const SYMFONY_GREAT = {"id":"symfony.great","translations":{"messages":{"en":"Symfony is awesome!","fr":"Symfony est g\u00e9nial !"}}};
 export const SYMFONY_WHAT = {"id":"symfony.what","translations":{"messages":{"en":"Symfony is %what%!","fr":"Symfony est %what%!"}}};
 export const SYMFONY_WHAT_1 = {"id":"symfony.what!","translations":{"messages":{"en":"Symfony is %what%! (should not conflict with the previous one.)","fr":"Symfony est %what%! (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}};
@@ -84,7 +83,6 @@ export declare const NOTIFICATION_COMMENT_CREATED: Message<{ 'messages+intl-icu'
 export declare const NOTIFICATION_COMMENT_CREATED_DESCRIPTION: Message<{ 'messages+intl-icu': { parameters: { 'title': string, 'link': string } } }, 'en'|'fr'>;
 export declare const POST_NUM_COMMENTS: Message<{ 'messages+intl-icu': { parameters: { 'count': number } }, 'foobar': { parameters: { '%count%': number } } }, 'en'|'fr'>;
 export declare const POST_NUM_COMMENTS_1: Message<{ 'messages+intl-icu': { parameters: { 'count': number } } }, 'en'|'fr'>;
-export declare const POST_NUM_COMMENTS_2: Message<{ 'messages+intl-icu': { parameters: { 'count': number } } }, 'en'|'fr'>;
 export declare const SYMFONY_GREAT: Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
 export declare const SYMFONY_WHAT: Message<{ 'messages': { parameters: { '%what%': string } } }, 'en'|'fr'>;
 export declare const SYMFONY_WHAT_1: Message<{ 'messages': { parameters: { '%what%': string } } }, 'en'|'fr'>;
@@ -151,7 +149,6 @@ TYPESCRIPT);
                     'notification.comment_created.description' => 'Your post "{title}" has received a new comment. You can read the comment by following <a href="{link}">this link</a>',
                     'post.num_comments' => '{count, plural, one {# comment} other {# comments}}',
                     'post.num_comments.' => '{count, plural, one {# comment} other {# comments}} (should not conflict with the previous one.)',
-                    'post.num_comments..' => '{count, plural, one {# comment} other {# comments}} (should not conflict with the previous one.)',
                 ],
                 'messages' => [
                     'symfony.great' => 'Symfony is awesome!',
@@ -181,7 +178,6 @@ TYPESCRIPT);
                     'notification.comment_created.description' => 'Votre article "{title}" a reçu un nouveau commentaire. Vous pouvez lire le commentaire en suivant <a href="{link}">ce lien</a>',
                     'post.num_comments' => '{count, plural, one {# commentaire} other {# commentaires}}',
                     'post.num_comments.' => '{count, plural, one {# commentaire} other {# commentaires}} (ne doit pas rentrer en conflit avec la traduction précédente)',
-                    'post.num_comments..' => '{count, plural, one {# commentaire} other {# commentaires}} (ne doit pas rentrer en conflit avec la traduction précédente)',
                 ],
                 'messages' => [
                     'symfony.great' => 'Symfony est génial !',
diff --git a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php
index 8ed7aa4f..5e22ac73 100644
--- a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php
+++ b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php
@@ -147,7 +147,7 @@ final class ComponentAttributesTest extends TestCase
             'data-controller' => 'foo live',
             'data-live-data-value' => '{}',
             'data-foo-name-value' => 'ryan',
-            'data-foo-some-array-value' => '[&quot;a&quot;,&quot;b&quot;]',
+            'data-foo-some-array-value' => '&#x5B;&quot;a&quot;,&quot;b&quot;&#x5D;',
         ], $attributes->all());
     }
 
diff --git a/src/TwigComponent/tests/Unit/ComponentFactoryTest.php b/src/TwigComponent/tests/Unit/ComponentFactoryTest.php
deleted file mode 100644
index fd1fd337..00000000
--- a/src/TwigComponent/tests/Unit/ComponentFactoryTest.php
+++ /dev/null
@@ -1,92 +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\TwigComponent\Tests\Unit;
-
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\DependencyInjection\ServiceLocator;
-use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
-use Symfony\UX\TwigComponent\ComponentFactory;
-use Symfony\UX\TwigComponent\ComponentTemplateFinderInterface;
-
-/**
- * @author Simon André <smn.andre@gmail.com>
- */
-class ComponentFactoryTest extends TestCase
-{
-    public function testMetadataForConfig(): void
-    {
-        $factory = new ComponentFactory(
-            $this->createMock(ComponentTemplateFinderInterface::class),
-            $this->createMock(ServiceLocator::class),
-            $this->createMock(PropertyAccessorInterface::class),
-            $this->createMock(EventDispatcherInterface::class),
-            ['foo' => ['key' => 'foo', 'template' => 'bar.html.twig']],
-            []
-        );
-
-        $metadata = $factory->metadataFor('foo');
-
-        $this->assertSame('foo', $metadata->getName());
-        $this->assertSame('bar.html.twig', $metadata->getTemplate());
-    }
-
-    public function testMetadataForResolveAlias(): void
-    {
-        $factory = new ComponentFactory(
-            $this->createMock(ComponentTemplateFinderInterface::class),
-            $this->createMock(ServiceLocator::class),
-            $this->createMock(PropertyAccessorInterface::class),
-            $this->createMock(EventDispatcherInterface::class),
-            [
-                'bar' => ['key' => 'bar', 'template' => 'bar.html.twig'],
-                'foo' => ['key' => 'foo', 'template' => 'foo.html.twig'],
-            ],
-            ['Foo\\Bar' => 'bar'],
-        );
-
-        $metadata = $factory->metadataFor('Foo\\Bar');
-
-        $this->assertSame('bar', $metadata->getName());
-        $this->assertSame('bar.html.twig', $metadata->getTemplate());
-    }
-
-    public function testMetadataForReuseAnonymousConfig(): void
-    {
-        $templateFinder = $this->createMock(ComponentTemplateFinderInterface::class);
-        $templateFinder->expects($this->atLeastOnce())
-            ->method('findAnonymousComponentTemplate')
-            ->with('foo')
-            ->willReturnOnConsecutiveCalls('foo.html.twig', 'bar.html.twig', 'bar.html.twig');
-
-        $factory = new ComponentFactory(
-            $templateFinder,
-            $this->createMock(ServiceLocator::class),
-            $this->createMock(PropertyAccessorInterface::class),
-            $this->createMock(EventDispatcherInterface::class),
-            [],
-            []
-        );
-
-        $metadata = $factory->metadataFor('foo');
-        $this->assertSame('foo', $metadata->getName());
-        $this->assertSame('foo.html.twig', $metadata->getTemplate());
-
-        $metadata = $factory->metadataFor('foo');
-        $this->assertSame('foo', $metadata->getName());
-        $this->assertSame('foo.html.twig', $metadata->getTemplate());
-
-        $metadata = $factory->metadataFor('foo');
-        $this->assertSame('foo', $metadata->getName());
-        $this->assertSame('foo.html.twig', $metadata->getTemplate());
-    }
-}
diff --git a/src/Vue/tests/Twig/VueComponentExtensionTest.php b/src/Vue/tests/Twig/VueComponentExtensionTest.php
index 6a539395..3c618cca 100644
--- a/src/Vue/tests/Twig/VueComponentExtensionTest.php
+++ b/src/Vue/tests/Twig/VueComponentExtensionTest.php
@@ -37,7 +37,7 @@ class VueComponentExtensionTest extends TestCase
         );
 
         $this->assertSame(
-            'data-controller="symfony--ux-vue--vue" data-symfony--ux-vue--vue-component-value="SubDir/MyComponent" data-symfony--ux-vue--vue-props-value="{&quot;fullName&quot;:&quot;Titouan Galopin&quot;}"',
+            'data-controller="symfony--ux-vue--vue" data-symfony--ux-vue--vue-component-value="SubDir&#x2F;MyComponent" data-symfony--ux-vue--vue-props-value="&#x7B;&quot;fullName&quot;&#x3A;&quot;Titouan&#x20;Galopin&quot;&#x7D;"',
             $rendered
         );
     }
@@ -53,7 +53,7 @@ class VueComponentExtensionTest extends TestCase
         $rendered = $extension->renderVueComponent('SubDir/MyComponent');
 
         $this->assertSame(
-            'data-controller="symfony--ux-vue--vue" data-symfony--ux-vue--vue-component-value="SubDir/MyComponent"',
+            'data-controller="symfony--ux-vue--vue" data-symfony--ux-vue--vue-component-value="SubDir&#x2F;MyComponent"',
             $rendered
         );
     }