Repository: symfony/ux
Pull request #1506 (2024-02-16 07:42:15)

[Site] Clean LiveMemory styles
Clean CSS files


Patch:
diff --git a/UPGRADE.md b/UPGRADE.md
deleted file mode 100644
index 263b4102..00000000
--- a/UPGRADE.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# UPGRADE
-
-## FROM 2.14 to 2.15
-
--   **Live Components**: Change `data-live-id` attributes to `id` #1484.
diff --git a/src/Autocomplete/CHANGELOG.md b/src/Autocomplete/CHANGELOG.md
index f4a3c44a..39032dcc 100644
--- a/src/Autocomplete/CHANGELOG.md
+++ b/src/Autocomplete/CHANGELOG.md
@@ -1,12 +1,5 @@
 # CHANGELOG
 
-## 2.15.0
-
--   Add doctrine/orm 3 support #1468
--   Allow passing extra options to the autocomplete fields #1322
--   Fix 2 bugs where TomSelect would reset when not necessary #1502
--   Add one missing German translation #1521
-
 ## 2.14.0
 
 -   Fixed behavior of Autocomplete when the underlying `select` or `option`
diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js
index 8e515104..49587145 100644
--- a/src/Autocomplete/assets/dist/controller.js
+++ b/src/Autocomplete/assets/dist/controller.js
@@ -116,7 +116,6 @@ class default_1 extends Controller {
     }
     resetTomSelect() {
         if (this.tomSelect) {
-            this.dispatchEvent('before-reset', { tomSelect: this.tomSelect });
             this.stopMutationObserver();
             const currentHtml = this.element.innerHTML;
             const currentValue = this.tomSelect.getValue();
@@ -144,7 +143,6 @@ class default_1 extends Controller {
                 subtree: true,
                 attributes: true,
                 characterData: true,
-                attributeOldValue: true,
             });
             this.isObserving = true;
         }
@@ -166,11 +164,7 @@ class default_1 extends Controller {
                         break;
                     }
                     if (mutation.target === this.element && mutation.attributeName === 'multiple') {
-                        const isNowMultiple = this.element.hasAttribute('multiple');
-                        const wasMultiple = mutation.oldValue === 'multiple';
-                        if (isNowMultiple !== wasMultiple) {
-                            requireReset = true;
-                        }
+                        requireReset = true;
                         break;
                     }
                     break;
@@ -197,22 +191,12 @@ class default_1 extends Controller {
         });
     }
     areOptionsEquivalent(newOptions) {
-        const filteredOriginalOptions = this.originalOptions.filter((option) => option.value !== '');
-        const filteredNewOptions = newOptions.filter((option) => option.value !== '');
-        const originalPlaceholderOption = this.originalOptions.find((option) => option.value === '');
-        const newPlaceholderOption = newOptions.find((option) => option.value === '');
-        console.log(originalPlaceholderOption, newPlaceholderOption);
-        if (originalPlaceholderOption &&
-            newPlaceholderOption &&
-            originalPlaceholderOption.text !== newPlaceholderOption.text) {
-            return false;
-        }
-        if (filteredOriginalOptions.length !== filteredNewOptions.length) {
+        if (this.originalOptions.length !== newOptions.length) {
             return false;
         }
         const normalizeOption = (option) => `${option.value}-${option.text}-${option.group}`;
-        const originalOptionsSet = new Set(filteredOriginalOptions.map(normalizeOption));
-        const newOptionsSet = new Set(filteredNewOptions.map(normalizeOption));
+        const originalOptionsSet = new Set(this.originalOptions.map(normalizeOption));
+        const newOptionsSet = new Set(newOptions.map(normalizeOption));
         return (originalOptionsSet.size === newOptionsSet.size &&
             [...originalOptionsSet].every((option) => newOptionsSet.has(option)));
     }
diff --git a/src/Autocomplete/assets/src/controller.ts b/src/Autocomplete/assets/src/controller.ts
index 412095e3..580552a2 100644
--- a/src/Autocomplete/assets/src/controller.ts
+++ b/src/Autocomplete/assets/src/controller.ts
@@ -325,7 +325,6 @@ export default class extends Controller {
 
     private resetTomSelect(): void {
         if (this.tomSelect) {
-            this.dispatchEvent('before-reset', { tomSelect: this.tomSelect });
             this.stopMutationObserver();
 
             // Grab the current HTML then restore it after destroying TomSelect
@@ -359,7 +358,6 @@ export default class extends Controller {
                 subtree: true,
                 attributes: true,
                 characterData: true,
-                attributeOldValue: true,
             });
             this.isObserving = true;
         }
@@ -386,11 +384,7 @@ export default class extends Controller {
                     }
 
                     if (mutation.target === this.element && mutation.attributeName === 'multiple') {
-                        const isNowMultiple = this.element.hasAttribute('multiple');
-                        const wasMultiple = mutation.oldValue === 'multiple';
-                        if (isNowMultiple !== wasMultiple) {
-                            requireReset = true;
-                        }
+                        requireReset = true;
 
                         break;
                     }
@@ -425,29 +419,14 @@ export default class extends Controller {
     }
 
     private areOptionsEquivalent(newOptions: Array<{ value: string; text: string; group: string | null }>): boolean {
-        // remove the empty option, which is added by TomSelect so may be missing from new options
-        const filteredOriginalOptions = this.originalOptions.filter((option) => option.value !== '');
-        const filteredNewOptions = newOptions.filter((option) => option.value !== '');
-
-        const originalPlaceholderOption = this.originalOptions.find((option) => option.value === '');
-        const newPlaceholderOption = newOptions.find((option) => option.value === '');
-        console.log(originalPlaceholderOption, newPlaceholderOption);
-        if (
-            originalPlaceholderOption &&
-            newPlaceholderOption &&
-            originalPlaceholderOption.text !== newPlaceholderOption.text
-        ) {
-            return false;
-        }
-
-        if (filteredOriginalOptions.length !== filteredNewOptions.length) {
+        if (this.originalOptions.length !== newOptions.length) {
             return false;
         }
 
         const normalizeOption = (option: { value: string; text: string; group: string | null }) =>
             `${option.value}-${option.text}-${option.group}`;
-        const originalOptionsSet = new Set(filteredOriginalOptions.map(normalizeOption));
-        const newOptionsSet = new Set(filteredNewOptions.map(normalizeOption));
+        const originalOptionsSet = new Set(this.originalOptions.map(normalizeOption));
+        const newOptionsSet = new Set(newOptions.map(normalizeOption));
 
         return (
             originalOptionsSet.size === newOptionsSet.size &&
diff --git a/src/Autocomplete/composer.json b/src/Autocomplete/composer.json
index 95aee140..bedb41fb 100644
--- a/src/Autocomplete/composer.json
+++ b/src/Autocomplete/composer.json
@@ -26,7 +26,6 @@
     "require": {
         "php": ">=8.1",
         "symfony/dependency-injection": "^6.3|^7.0",
-        "symfony/deprecation-contracts": "^2.5|^3",
         "symfony/http-foundation": "^6.3|^7.0",
         "symfony/http-kernel": "^6.3|^7.0",
         "symfony/property-access": "^6.3|^7.0",
@@ -35,7 +34,7 @@
     "require-dev": {
         "doctrine/collections": "^1.6.8|^2.0",
         "doctrine/doctrine-bundle": "^2.4.3",
-        "doctrine/orm": "^2.9.4|^3.0",
+        "doctrine/orm": "^2.9.4",
         "fakerphp/faker": "^1.22",
         "mtdowling/jmespath.php": "^2.6",
         "symfony/form": "^6.3|^7.0",
diff --git a/src/Autocomplete/src/Doctrine/EntityMetadata.php b/src/Autocomplete/src/Doctrine/EntityMetadata.php
index 7a01d0d8..6a5807fb 100644
--- a/src/Autocomplete/src/Doctrine/EntityMetadata.php
+++ b/src/Autocomplete/src/Doctrine/EntityMetadata.php
@@ -42,41 +42,13 @@ class EntityMetadata
     }
 
     public function getPropertyMetadata(string $propertyName): array
-    {
-        trigger_deprecation('symfony/ux-autocomplete', '2.15.0', 'Calling EntityMetadata::getPropertyMetadata() is deprecated. You should stop using it, as it will be removed in the future.');
-
-        try {
-            return $this->getFieldMetadata($propertyName);
-        } catch (\InvalidArgumentException $e) {
-            return $this->getAssociationMetadata($propertyName);
-        }
-    }
-
-    /**
-     * @internal
-     *
-     * @return array<string, mixed>
-     */
-    public function getFieldMetadata(string $propertyName): array
     {
         if (\array_key_exists($propertyName, $this->metadata->fieldMappings)) {
-            // Cast to array, because in doctrine/orm:^3.0; $metadata will be a FieldMapping object
-            return (array) $this->metadata->fieldMappings[$propertyName];
+            return $this->metadata->fieldMappings[$propertyName];
         }
 
-        throw new \InvalidArgumentException(sprintf('The "%s" field does not exist in the "%s" entity.', $propertyName, $this->metadata->getName()));
-    }
-
-    /**
-     * @internal
-     *
-     * @return array<string, mixed>
-     */
-    public function getAssociationMetadata(string $propertyName): array
-    {
         if (\array_key_exists($propertyName, $this->metadata->associationMappings)) {
-            // Cast to array, because in doctrine/orm:^3.0; $metadata will be an AssociationMapping object
-            return (array) $this->metadata->associationMappings[$propertyName];
+            return $this->metadata->associationMappings[$propertyName];
         }
 
         throw new \InvalidArgumentException(sprintf('The "%s" field does not exist in the "%s" entity.', $propertyName, $this->metadata->getName()));
@@ -84,11 +56,7 @@ class EntityMetadata
 
     public function getPropertyDataType(string $propertyName): string
     {
-        if (\array_key_exists($propertyName, $this->metadata->fieldMappings)) {
-            return $this->getFieldMetadata($propertyName)['type'];
-        }
-
-        return $this->getAssociationMetadata($propertyName)['type'];
+        return $this->getPropertyMetadata($propertyName)['type'];
     }
 
     public function getIdValue(object $entity): string
diff --git a/src/Autocomplete/src/Doctrine/EntitySearchUtil.php b/src/Autocomplete/src/Doctrine/EntitySearchUtil.php
index 7cdf1057..44986dad 100644
--- a/src/Autocomplete/src/Doctrine/EntitySearchUtil.php
+++ b/src/Autocomplete/src/Doctrine/EntitySearchUtil.php
@@ -60,7 +60,7 @@ class EntitySearchUtil
                 }
 
                 $originalPropertyName = $associatedProperties[0];
-                $originalPropertyMetadata = $entityMetadata->getAssociationMetadata($originalPropertyName);
+                $originalPropertyMetadata = $entityMetadata->getPropertyMetadata($originalPropertyName);
                 $associatedEntityDto = $this->metadataFactory->create($originalPropertyMetadata['targetEntity']);
 
                 for ($i = 0; $i < $numAssociatedProperties - 1; ++$i) {
@@ -75,8 +75,9 @@ class EntitySearchUtil
                     }
 
                     if ($i < $numAssociatedProperties - 2) {
-                        $propertyMetadata = $associatedEntityDto->getAssociationMetadata($associatedPropertyName);
-                        $associatedEntityDto = $this->metadataFactory->create($propertyMetadata['targetEntity']);
+                        $propertyMetadata = $associatedEntityDto->getPropertyMetadata($associatedPropertyName);
+                        $targetEntity = $propertyMetadata['targetEntity'];
+                        $associatedEntityDto = $this->metadataFactory->create($targetEntity);
                     }
                 }
 
diff --git a/src/Autocomplete/translations/AutocompleteBundle.de.php b/src/Autocomplete/translations/AutocompleteBundle.de.php
index d26583b4..059119f8 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.de.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.de.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Lade weitere Ergebnisse...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Keine Übereinstimmungen gefunden',
     'No more results' => 'Keine weiteren Ergebnisse',
 ];
diff --git a/src/Chartjs/CHANGELOG.md b/src/Chartjs/CHANGELOG.md
index 1d0c4865..0e83ec18 100644
--- a/src/Chartjs/CHANGELOG.md
+++ b/src/Chartjs/CHANGELOG.md
@@ -1,9 +1,5 @@
 # CHANGELOG
 
-## 2.15.0
-
--   Remove restriction that prevented Chart.js 3.9 #1518
-
 ## 2.14.0
 
 -   Add support for Chart.js version 4
diff --git a/src/Chartjs/assets/package.json b/src/Chartjs/assets/package.json
index b121bce5..9cd0e5dc 100644
--- a/src/Chartjs/assets/package.json
+++ b/src/Chartjs/assets/package.json
@@ -21,7 +21,7 @@
     },
     "peerDependencies": {
         "@hotwired/stimulus": "^3.0.0",
-        "chart.js": "^3.4.1 || ^4.0"
+        "chart.js": "^3.4.1 <3.9 || ^4.0"
     },
     "devDependencies": {
         "@hotwired/stimulus": "^3.0.0",
diff --git a/src/LiveComponent/CHANGELOG.md b/src/LiveComponent/CHANGELOG.md
index 233ed1ba..8903ea2e 100644
--- a/src/LiveComponent/CHANGELOG.md
+++ b/src/LiveComponent/CHANGELOG.md
@@ -1,58 +1,8 @@
 # CHANGELOG
 
-## 2.16.0
-
--   [BC BREAK] The `data-action-name` attribute behavior was removed in favor of
-    using Stimulus "action parameters" and `data-live-action-param`. This is a
-    breaking change if you were using the `data-action-name` attribute directly
-    in your templates.
-
-    To upgrade your application, follow these changes:
-
-    ```diff
-    <button
-        data-action="live#action"
-    -    data-action-name="debounce(300)|save"
-    +    data-live-action-param="debounce(300)|save"
-    >Save</button>
-    ```
-
-    To pass arguments to an action, also use the Stimulus "action parameters" syntax:
-
-    ```diff
-    <button
-        data-action="live#action"
-    -     data-action-name="addItem(id={{ item.id }}, itemName=CustomItem)"
-    +     data-live-action-param="addItem"
-    +     data-live-id-param="{{ item.id }}"
-    +     data-live-item-name-param="CustomItem"
-    >Add Item</button>
-    ```
-
-    Additionally, the `prevent` modifier (e.g. `prevent|save`) was removed. Replace
-    this with the standard Stimulus `:prevent` action option:
-
-    ```diff
-    <button
-    -    data-action="live#action
-    +    data-action="live#action:prevent"
-    -    data-action-name="prevent|save"
-    +    data-live-action-param="save"
-     >Save</button>
-    ```
-
--   [BC BREAK] The `data-event` attribute was removed in favor of using Stimulus
-    "action parameters": rename `data-event` to `data-live-event-param`. Additionally,
-    if you were passing arguments to the event name, use action parameter attributes
-    for those as well - e.g. `data-live-foo-param="bar"`.
-
 ## 2.15.0
 
--   [BC BREAK] The `data-live-id` attribute was changed to `id` #1484
--   Fixed child handling bug during re-rendering introduced with the
-    new morphing library in 2.14.0 #1484
--   Fix bug where the active input would maintain its value, but lose its cursor position #1501
--   Restrict Twig 3.9 for now #1486
+-   [BC BREAK] The `data-live-id` attribute was changed to `id`.
 
 ## 2.14.1
 
diff --git a/src/LiveComponent/assets/dist/Directive/directives_parser.d.ts b/src/LiveComponent/assets/dist/Directive/directives_parser.d.ts
index 7cdef915..cd08322f 100644
--- a/src/LiveComponent/assets/dist/Directive/directives_parser.d.ts
+++ b/src/LiveComponent/assets/dist/Directive/directives_parser.d.ts
@@ -5,6 +5,7 @@ export interface DirectiveModifier {
 export interface Directive {
     action: string;
     args: string[];
+    named: any;
     modifiers: DirectiveModifier[];
     getString: {
         (): string;
diff --git a/src/LiveComponent/assets/dist/live_controller.d.ts b/src/LiveComponent/assets/dist/live_controller.d.ts
index e80e322f..fa533b60 100644
--- a/src/LiveComponent/assets/dist/live_controller.d.ts
+++ b/src/LiveComponent/assets/dist/live_controller.d.ts
@@ -95,10 +95,10 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
     disconnect(): void;
     update(event: any): void;
     action(event: any): void;
+    emit(event: Event): void;
+    emitUp(event: Event): void;
+    emitSelf(event: Event): void;
     $render(): Promise<import("./Backend/BackendResponse").default>;
-    emit(event: any): void;
-    emitUp(event: any): void;
-    emitSelf(event: any): void;
     $updateModel(model: string, value: any, shouldRender?: boolean, debounce?: number | boolean): Promise<import("./Backend/BackendResponse").default>;
     propsUpdatedFromParentValueChanged(): void;
     fingerprintValueChanged(): void;
diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js
index fedc04fb..2968fd82 100644
--- a/src/LiveComponent/assets/dist/live_controller.js
+++ b/src/LiveComponent/assets/dist/live_controller.js
@@ -6,8 +6,10 @@ function parseDirectives(content) {
         return directives;
     }
     let currentActionName = '';
+    let currentArgumentName = '';
     let currentArgumentValue = '';
     let currentArguments = [];
+    let currentNamedArguments = {};
     let currentModifiers = [];
     let state = 'action';
     const getLastActionName = function () {
@@ -23,30 +25,52 @@ function parseDirectives(content) {
         directives.push({
             action: currentActionName,
             args: currentArguments,
+            named: currentNamedArguments,
             modifiers: currentModifiers,
             getString: () => {
                 return content;
             }
         });
         currentActionName = '';
+        currentArgumentName = '';
         currentArgumentValue = '';
         currentArguments = [];
+        currentNamedArguments = {};
         currentModifiers = [];
         state = 'action';
     };
     const pushArgument = function () {
-        currentArguments.push(currentArgumentValue.trim());
+        const mixedArgTypesError = () => {
+            throw new Error(`Normal and named arguments cannot be mixed inside "${currentActionName}()"`);
+        };
+        if (currentArgumentName) {
+            if (currentArguments.length > 0) {
+                mixedArgTypesError();
+            }
+            currentNamedArguments[currentArgumentName.trim()] = currentArgumentValue;
+        }
+        else {
+            if (Object.keys(currentNamedArguments).length > 0) {
+                mixedArgTypesError();
+            }
+            currentArguments.push(currentArgumentValue.trim());
+        }
+        currentArgumentName = '';
         currentArgumentValue = '';
     };
     const pushModifier = function () {
         if (currentArguments.length > 1) {
             throw new Error(`The modifier "${currentActionName}()" does not support multiple arguments.`);
         }
+        if (Object.keys(currentNamedArguments).length > 0) {
+            throw new Error(`The modifier "${currentActionName}()" does not support named arguments.`);
+        }
         currentModifiers.push({
             name: currentActionName,
             value: currentArguments.length > 0 ? currentArguments[0] : null,
         });
         currentActionName = '';
+        currentArgumentName = '';
         currentArguments = [];
         state = 'action';
     };
@@ -80,6 +104,11 @@ function parseDirectives(content) {
                     pushArgument();
                     break;
                 }
+                if (char === '=') {
+                    currentArgumentName = currentArgumentValue;
+                    currentArgumentValue = '';
+                    break;
+                }
                 currentArgumentValue += char;
                 break;
             case 'after_arguments':
@@ -296,7 +325,7 @@ function getAllModelDirectiveFromElements(element) {
     }
     const directives = parseDirectives(element.dataset.model);
     directives.forEach((directive) => {
-        if (directive.args.length > 0) {
+        if (directive.args.length > 0 || directive.named.length > 0) {
             throw new Error(`The data-model="${element.dataset.model}" format is invalid: it does not support passing arguments to the model.`);
         }
         directive.action = normalizeModelName(directive.action);
@@ -313,7 +342,7 @@ function getModelDirectiveFromElement(element, throwOnMissing = true) {
         if (formElement && 'model' in formElement.dataset) {
             const directives = parseDirectives(formElement.dataset.model || '*');
             const directive = directives[0];
-            if (directive.args.length > 0) {
+            if (directive.args.length > 0 || directive.named.length > 0) {
                 throw new Error(`The data-model="${formElement.dataset.model}" format is invalid: it does not support passing arguments to the model.`);
             }
             directive.action = normalizeModelName(element.getAttribute('name'));
@@ -588,7 +617,7 @@ var Idiomorph = (function () {
          * @returns {boolean}
          */
         function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
-            return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
+            return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
         }
 
         /**
@@ -1349,7 +1378,6 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
         syncAttributes(newElement, oldElement);
     });
     Idiomorph.morph(rootFromElement, rootToElement, {
-        ignoreActiveValue: true,
         callbacks: {
             beforeNodeMorphed: (fromEl, toEl) => {
                 if (!(fromEl instanceof Element) || !(toEl instanceof Element)) {
@@ -2913,18 +2941,15 @@ class LiveControllerDefault extends Controller {
         this.updateModelFromElementEvent(event.currentTarget, null);
     }
     action(event) {
-        const params = event.params;
-        if (!params.action) {
-            throw new Error(`No action name provided on element: ${getElementAsTagText(event.currentTarget)}. Did you forget to add the "data-live-action-param" attribute?`);
-        }
-        const rawAction = params.action;
-        const actionArgs = Object.assign({}, params);
-        delete actionArgs.action;
+        const rawAction = event.currentTarget.dataset.actionName;
         const directives = parseDirectives(rawAction);
         let debounce = false;
         directives.forEach((directive) => {
             let pendingFiles = {};
             const validModifiers = new Map();
+            validModifiers.set('prevent', () => {
+                event.preventDefault();
+            });
             validModifiers.set('stop', () => {
                 event.stopPropagation();
             });
@@ -2959,15 +2984,12 @@ class LiveControllerDefault extends Controller {
                 }
                 delete this.pendingFiles[key];
             }
-            this.component.action(directive.action, actionArgs, debounce);
+            this.component.action(directive.action, directive.named, debounce);
             if (getModelDirectiveFromElement(event.currentTarget, false)) {
                 this.pendingActionTriggerModelElement = event.currentTarget;
             }
         });
     }
-    $render() {
-        return this.component.render();
-    }
     emit(event) {
         this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => {
             this.component.emit(name, data, nameMatch);
@@ -2983,6 +3005,9 @@ class LiveControllerDefault extends Controller {
             this.component.emitSelf(name, data);
         });
     }
+    $render() {
+        return this.component.render();
+    }
     $updateModel(model, value, shouldRender = true, debounce = true) {
         return this.component.set(model, value, shouldRender, debounce);
     }
@@ -2993,13 +3018,11 @@ class LiveControllerDefault extends Controller {
         this.component.fingerprint = this.fingerprintValue;
     }
     getEmitDirectives(event) {
-        const params = event.params;
-        if (!params.event) {
-            throw new Error(`No event name provided on element: ${getElementAsTagText(event.currentTarget)}. Did you forget to add the "data-live-event-param" attribute?`);
+        const element = event.currentTarget;
+        if (!element.dataset.event) {
+            throw new Error(`No data-event attribute found on element: ${getElementAsTagText(element)}`);
         }
-        const eventInfo = params.event;
-        const eventArgs = Object.assign({}, params);
-        delete eventArgs.event;
+        const eventInfo = element.dataset.event;
         const directives = parseDirectives(eventInfo);
         const emits = [];
         directives.forEach((directive) => {
@@ -3015,7 +3038,7 @@ class LiveControllerDefault extends Controller {
             });
             emits.push({
                 name: directive.action,
-                data: eventArgs,
+                data: directive.named,
                 nameMatch,
             });
         });
diff --git a/src/LiveComponent/assets/package.json b/src/LiveComponent/assets/package.json
index c93a7868..a0d5dba4 100644
--- a/src/LiveComponent/assets/package.json
+++ b/src/LiveComponent/assets/package.json
@@ -27,7 +27,7 @@
         }
     },
     "dependencies": {
-        "idiomorph": "https://github.com/bigskysoftware/idiomorph.git"
+        "idiomorph": "^0.3.0"
     },
     "peerDependencies": {
         "@hotwired/stimulus": "^3.0.0"
diff --git a/src/LiveComponent/assets/src/Directive/directives_parser.ts b/src/LiveComponent/assets/src/Directive/directives_parser.ts
index 0b5f1c9d..2e2bc85d 100644
--- a/src/LiveComponent/assets/src/Directive/directives_parser.ts
+++ b/src/LiveComponent/assets/src/Directive/directives_parser.ts
@@ -25,6 +25,10 @@ export interface Directive {
      * An array of unnamed arguments passed to the action
      */
     args: string[];
+    /**
+     * An object of named arguments
+     */
+    named: any;
     /**
      * Any modifiers applied to the action
      */
@@ -37,8 +41,17 @@ export interface Directive {
  * into an array of directives, with this format:
  *
  *      [
- *          { action: 'addClass', args: ['foo'], modifiers: [] },
- *          { action: 'removeAttribute', args: ['bar'], modifiers: [] }
+ *          { action: 'addClass', args: ['foo'], named: {}, modifiers: [] },
+ *          { action: 'removeAttribute', args: ['bar'], named: {}, modifiers: [] }
+ *      ]
+ *
+ * This also handles named arguments
+ *
+ *      save(foo=bar, baz=bazzles)
+ *
+ * Which would return:
+ *      [
+ *          { action: 'save', args: [], named: { foo: 'bar', baz: 'bazzles }, modifiers: [] }
  *      ]
  *
  * @param {string} content The value of the attribute
@@ -51,8 +64,10 @@ export function parseDirectives(content: string|null): Directive[] {
     }
 
     let currentActionName = '';
+    let currentArgumentName = '';
     let currentArgumentValue = '';
     let currentArguments: string[] = [];
+    let currentNamedArguments: any = {};
     let currentModifiers: { name: string, value: string | null }[] = [];
     let state = 'action';
 
@@ -71,6 +86,7 @@ export function parseDirectives(content: string|null): Directive[] {
         directives.push({
             action: currentActionName,
             args: currentArguments,
+            named: currentNamedArguments,
             modifiers: currentModifiers,
             getString: () => {
                 // TODO - make a string representation of JUST this directive
@@ -79,15 +95,36 @@ export function parseDirectives(content: string|null): Directive[] {
             }
         });
         currentActionName = '';
+        currentArgumentName = '';
         currentArgumentValue = '';
         currentArguments = [];
+        currentNamedArguments = {};
         currentModifiers = [];
         state = 'action';
     }
     const pushArgument = function() {
-        // value is trimmed to avoid space after ","
-        // "foo, bar"
-        currentArguments.push(currentArgumentValue.trim());
+        const mixedArgTypesError = () => {
+            throw new Error(`Normal and named arguments cannot be mixed inside "${currentActionName}()"`)
+        }
+
+        if (currentArgumentName) {
+            if (currentArguments.length > 0) {
+                mixedArgTypesError();
+            }
+
+            // argument names are also trimmed to avoid space after ","
+            // "foo=bar, baz=bazzles"
+            currentNamedArguments[currentArgumentName.trim()] = currentArgumentValue;
+        } else {
+            if (Object.keys(currentNamedArguments).length > 0) {
+                mixedArgTypesError();
+            }
+
+            // value is trimmed to avoid space after ","
+            // "foo, bar"
+            currentArguments.push(currentArgumentValue.trim());
+        }
+        currentArgumentName = '';
         currentArgumentValue = '';
     }
 
@@ -96,11 +133,16 @@ export function parseDirectives(content: string|null): Directive[] {
             throw new Error(`The modifier "${currentActionName}()" does not support multiple arguments.`)
         }
 
+        if (Object.keys(currentNamedArguments).length > 0) {
+            throw new Error(`The modifier "${currentActionName}()" does not support named arguments.`)
+        }
+
         currentModifiers.push({
             name: currentActionName,
             value: currentArguments.length > 0 ? currentArguments[0] : null,
         });
         currentActionName = '';
+        currentArgumentName = '';
         currentArguments = [];
         state = 'action';
     }
@@ -154,6 +196,14 @@ export function parseDirectives(content: string|null): Directive[] {
                     break;
                 }
 
+                if (char === '=') {
+                    // this is a named argument!
+                    currentArgumentName = currentArgumentValue;
+                    currentArgumentValue = '';
+
+                    break;
+                }
+
                 // add next character to argument
                 currentArgumentValue += char;
 
diff --git a/src/LiveComponent/assets/src/dom_utils.ts b/src/LiveComponent/assets/src/dom_utils.ts
index 4970012f..b756f3de 100644
--- a/src/LiveComponent/assets/src/dom_utils.ts
+++ b/src/LiveComponent/assets/src/dom_utils.ts
@@ -141,7 +141,7 @@ export function getAllModelDirectiveFromElements(element: HTMLElement): Directiv
     const directives = parseDirectives(element.dataset.model);
 
     directives.forEach((directive) => {
-        if (directive.args.length > 0) {
+        if (directive.args.length > 0 || directive.named.length > 0) {
             throw new Error(
                 `The data-model="${element.dataset.model}" format is invalid: it does not support passing arguments to the model.`
             );
@@ -167,7 +167,7 @@ export function getModelDirectiveFromElement(element: HTMLElement, throwOnMissin
             const directives = parseDirectives(formElement.dataset.model || '*');
             const directive = directives[0];
 
-            if (directive.args.length > 0) {
+            if (directive.args.length > 0 || directive.named.length > 0) {
                 throw new Error(
                     `The data-model="${formElement.dataset.model}" format is invalid: it does not support passing arguments to the model.`
                 );
diff --git a/src/LiveComponent/assets/src/live_controller.ts b/src/LiveComponent/assets/src/live_controller.ts
index 5198a33e..25025c55 100644
--- a/src/LiveComponent/assets/src/live_controller.ts
+++ b/src/LiveComponent/assets/src/live_controller.ts
@@ -118,26 +118,23 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
     }
 
     action(event: any) {
-        const params = event.params;
-        if (!params.action) {
-            throw new Error(
-                `No action name provided on element: ${getElementAsTagText(
-                    event.currentTarget
-                )}. Did you forget to add the "data-live-action-param" attribute?`
-            );
-        }
-        const rawAction = params.action;
-        // all other params are considered action arguments
-        const actionArgs = { ...params };
-        delete actionArgs.action;
-
-        // data-live-action-param="debounce(1000)|save"
+        // using currentTarget means that the data-action and data-action-name
+        // must live on the same element: you can't add
+        // data-action="click->live#action" on a parent element and
+        // expect it to use the data-action-name from the child element
+        // that actually received the click
+        const rawAction = event.currentTarget.dataset.actionName;
+
+        // data-action-name="prevent|debounce(1000)|save"
         const directives = parseDirectives(rawAction);
         let debounce: number | boolean = false;
 
         directives.forEach((directive) => {
             let pendingFiles: { [key: string]: HTMLInputElement } = {};
             const validModifiers: Map<string, (modifier: DirectiveModifier) => void> = new Map();
+            validModifiers.set('prevent', () => {
+                event.preventDefault();
+            });
             validModifiers.set('stop', () => {
                 event.stopPropagation();
             });
@@ -179,7 +176,7 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
                 }
                 delete this.pendingFiles[key];
             }
-            this.component.action(directive.action, actionArgs, debounce);
+            this.component.action(directive.action, directive.named, debounce);
 
             // possible case where this element is also a "model" element
             // if so, to be safe, slightly delay the action so that the
@@ -191,28 +188,28 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
         });
     }
 
-    $render() {
-        return this.component.render();
-    }
-
-    emit(event: any) {
+    emit(event: Event) {
         this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => {
             this.component.emit(name, data, nameMatch);
         });
     }
 
-    emitUp(event: any) {
+    emitUp(event: Event) {
         this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => {
             this.component.emitUp(name, data, nameMatch);
         });
     }
 
-    emitSelf(event: any) {
+    emitSelf(event: Event) {
         this.getEmitDirectives(event).forEach(({ name, data }) => {
             this.component.emitSelf(name, data);
         });
     }
 
+    $render() {
+        return this.component.render();
+    }
+
     /**
      * Update a model value.
      *
@@ -233,19 +230,13 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
         this.component.fingerprint = this.fingerprintValue;
     }
 
-    private getEmitDirectives(event: any): Array<{ name: string; data: any; nameMatch: string | null }> {
-        const params = event.params;
-        if (!params.event) {
-            throw new Error(
-                `No event name provided on element: ${getElementAsTagText(
-                    event.currentTarget
-                )}. Did you forget to add the "data-live-event-param" attribute?`
-            );
+    private getEmitDirectives(event: Event): Array<{ name: string; data: any; nameMatch: string | null }> {
+        const element = event.currentTarget as HTMLElement;
+        if (!element.dataset.event) {
+            throw new Error(`No data-event attribute found on element: ${getElementAsTagText(element)}`);
         }
-        const eventInfo = params.event;
-        // all other params are considered event arguments
-        const eventArgs = { ...params };
-        delete eventArgs.event;
+
+        const eventInfo = element.dataset.event;
 
         // data-event="name(product_list)|some_event"
         const directives = parseDirectives(eventInfo);
@@ -264,7 +255,7 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
 
             emits.push({
                 name: directive.action,
-                data: eventArgs,
+                data: directive.named,
                 nameMatch,
             });
         });
diff --git a/src/LiveComponent/assets/src/morphdom.ts b/src/LiveComponent/assets/src/morphdom.ts
index 92a31080..7dba0668 100644
--- a/src/LiveComponent/assets/src/morphdom.ts
+++ b/src/LiveComponent/assets/src/morphdom.ts
@@ -53,12 +53,6 @@ export function executeMorphdom(
     });
 
     Idiomorph.morph(rootFromElement, rootToElement, {
-        // We handle updating the value of fields that have been changed
-        // since the HTML was requested. However, the active element is
-        // a special case: replacing the value isn't enough. We need to
-        // prevent the value from being changed in the first place so the
-        // user's cursor position is maintained.
-        ignoreActiveValue: true,
         callbacks: {
             beforeNodeMorphed: (fromEl: Element, toEl: Element) => {
                 // Idiomorph loop also over Text node
diff --git a/src/LiveComponent/composer.json b/src/LiveComponent/composer.json
index 77e5ef7f..83b9f2af 100644
--- a/src/LiveComponent/composer.json
+++ b/src/LiveComponent/composer.json
@@ -29,7 +29,7 @@
         "php": ">=8.1",
         "symfony/property-access": "^5.4.5|^6.0|^7.0",
         "symfony/ux-twig-component": "^2.8",
-        "twig/twig": "~3.8.0"
+        "twig/twig": "^2.14.7|~3.8.0"
     },
     "require-dev": {
         "doctrine/annotations": "^1.0",
diff --git a/src/LiveComponent/doc/index.rst b/src/LiveComponent/doc/index.rst
index 3c2d503c..4a4e3b29 100644
--- a/src/LiveComponent/doc/index.rst
+++ b/src/LiveComponent/doc/index.rst
@@ -96,9 +96,9 @@ needed if you're using AssetMapper):
     $ yarn install --force
     $ yarn watch
 
-If your project is localized in different languages (either via the `_locale route parameter`_
-or by `setting the locale in the request`_) add the ``{ _locale}`` attribute to
-the UX Live Components route definition to keep the locale between re-renders:
+In case your project `localizes its URLs`_ by adding the special
+``{_locale}`` parameter to the paths of its route definitions,
+you need to do the same with the UX Live Components route definition:
 
 .. code-block:: diff
 
@@ -1036,21 +1036,14 @@ the work::
         // ...
     }
 
-.. versionadded:: 2.16
-
-    The ``data-live-action-param`` attribute way of specifying the action
-    was added in Live Components 2.16. Previously, this was done with
-    ``data-live-action-name``.
-
-To call this, trigger the ``action`` method on the ``live`` Stimulus
-controller and pass ``resetMax`` as a `Stimulus action parameter`_ called
-``action``:
+To call this, add ``data-action="live#action"`` and ``data-action-name``
+to an element (e.g. a button or form):
 
 .. code-block:: html+twig
 
     <button
         data-action="live#action"
-        data-live-action-param="resetMax"
+        data-action-name="resetMax"
     >Reset Min/Max</button>
 
 Done! When the user clicks this button, a POST request will be sent that
@@ -1065,12 +1058,14 @@ You can also add several "modifiers" to the action:
     <form>
         <button
             data-action="live#action"
-            data-live-action-param="debounce(300)|save"
+            data-action-name="prevent|debounce(300)|save"
         >Save</button>
     </form>
 
-The ``debounce(300)`` adds 300ms of "debouncing" before the action is executed.
-In other words, if you click really fast 5 times, only one Ajax request will be made!
+The ``prevent`` modifier would prevent the form from submitting
+(``event.preventDefault()``). The ``debounce(300)`` modifier will add
+300ms of "debouncing" before the action is executed. In other words, if
+you click really fast 5 times, only one Ajax request will be made!
 
 Actions & Services
 ~~~~~~~~~~~~~~~~~~
@@ -1104,28 +1099,22 @@ This means that, for example, you can use action autowiring::
 Actions & Arguments
 ~~~~~~~~~~~~~~~~~~~
 
-.. versionadded:: 2.16
+.. versionadded:: 2.1
 
-    The ``data-live-{NAME}-param`` attribute way of specifying action
-    arguments was added in Live Components 2.16. Previously, this was done
-    inside the ``data-live-action-name`` attribute.
+    The ability to pass arguments to actions was added in version 2.1.
 
-You can also pass arguments to your action by adding each as a
-`Stimulus action parameter`_:
+You can also provide custom arguments to your action:
 
 .. code-block:: html+twig
 
     <form>
         <button
             data-action="live#action"
-            data-live-action-param="addItem"
-
-            data-live-id-param="{{ item.id }}"
-            data-live-item-name-param="CustomItem"
+            data-action-name="addItem(id={{ item.id }}, itemName=CustomItem)"
         >Add Item</button>
     </form>
 
-In your component, to allow each argument to be passed, add
+In your component, to allow each argument to be passed, we need to add
 the ``#[LiveArg()]`` attribute::
 
     // src/Components/ItemList.php
@@ -1146,6 +1135,10 @@ the ``#[LiveArg()]`` attribute::
         }
     }
 
+Normally, the argument name in PHP - e.g. ``$id`` - should match the
+argument name used in Twig ``id={{ item.id }}``. But if they don't
+match, you can pass an argument to ``LiveArg``, like we did with ``itemName``.
+
 Actions and CSRF Protection
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1223,11 +1216,10 @@ to handle the files and tell the component when the file should be sent:
 
 .. code-block:: html+twig
 
-    <input type="file" name="my_file" />
-    <button
-        data-action="live#action"
-        data-live-action-param="files|my_action"
-    />
+    <p>
+        <input type="file" name="my_file" />
+        <button data-action="live#action" data-action-name="files|my_action" />
+    </p>
 
 To send a file (or files) with an action use ``files`` modifier.
 Without an argument it will send all pending files to your action.
@@ -1241,11 +1233,11 @@ You can also specify a modifier parameter to choose which files should be upload
         <input type="file" name="multiple[]" multiple />
 
         {# Send only file from first input #}
-        <button data-action="live#action" data-live-action-param="files(my_file)|myAction" />
+        <button data-action="live#action" data-action-name="files(my_file)|myAction" />
         {# You can chain modifiers to send multiple files #}
-        <button data-action="live#action" data-live-action-param="files(my_file)|files(multiple[])|myAction" />
+        <button data-action="live#action" data-action-name="files(my_file)|files(multiple[])|myAction" />
         {# Or send all pending files #}
-        <button data-action="live#action" data-live-action-param="files|myAction" />
+        <button data-action="live#action" data-action-name="files|myAction" />
     </p>
 
 The files will be available in a regular ``$request->files`` files bag::
@@ -1464,8 +1456,8 @@ Next, tell the ``form`` element to use this action:
 
     {{ form_start(form, {
         attr: {
-            'data-action': 'live#action:prevent',
-            'data-live-action-param': 'save'
+            'data-action': 'live#action',
+            'data-action-name': 'prevent|save'
         }
     }) }}
 
@@ -1765,8 +1757,7 @@ and ``removeComment()`` actions:
             {% for key, commentForm in form.comments %}
                 <button
                     data-action="live#action"
-                    data-live-action-param="removeComment"
-                    data-live-index-param="{{ key }}"
+                    data-action-name="removeComment(index={{ key }})"
                     type="button"
                 >X</button>
 
@@ -1778,7 +1769,7 @@ and ``removeComment()`` actions:
 
             <button
                 data-action="live#action"
-                data-live-action-param="addComment"
+                data-action-name="addComment"
                 type="button"
             >+ Add Comment</button>
 
@@ -2181,8 +2172,8 @@ re-rendered. In your template, render errors using an ``_errors`` variable:
 
     <button
         type="submit"
-        data-action="live#action:prevent"
-        data-live-action-param="save"
+        data-action="live#action"
+        data-action-name="prevent|save"
     >Save</button>
 
 Once a component has been validated, the component will "remember" that
@@ -2266,9 +2257,9 @@ Or override the ``loadingContent`` block:
 
 .. code-block:: twig
 
-    {% component SomeHeavyComponent with { defer: true } %}
+    {% component SomeHeavyComponent with { defer: true }) }}
         {% block loadingContent %}Loading...{% endblock %}
-    {% endcomponent %}
+    {{ end_component() }}
 
 To change the initial tag from a ``div`` to something else, use the ``loading-tag`` option:
 
@@ -2456,18 +2447,13 @@ Emitting an Event
 
 There are three ways to emit an event:
 
-.. versionchanged:: 2.16
-
-    The ``data-live-event-param`` attribute was added in Live Components 2.16.
-    Previously, it was called ``data-event``.
-
 1. From Twig:
 
    .. code-block:: html+twig
 
        <button
            data-action="live#emit"
-           data-live-event-param="productAdded"
+           data-event="productAdded"
        >
 
 2. From your PHP component via ``ComponentToolsTrait``::
@@ -2564,7 +2550,7 @@ If you want to emit an event to only the parent components, use the
 
     <button
         data-action="live#emitUp"
-        data-live-event-param="productAdded"
+        data-event="productAdded"
     >
 
 Or, in PHP::
@@ -2581,7 +2567,7 @@ use the ``name()`` modifier:
 
     <button
         data-action="live#emit"
-        data-live-event-param="name(ProductList)|productAdded"
+        data-event="name(ProductList)|productAdded"
     >
 
 Or, in PHP::
@@ -2597,7 +2583,7 @@ To emit an event to only yourself, use the ``emitSelf()`` method:
 
     <button
         data-action="live#emitSelf"
-        data-live-event-param="productAdded"
+        data-event="productAdded"
     >
 
 Or, in PHP::
@@ -2780,7 +2766,7 @@ suppose your child component has:
 
 .. code-block:: html
 
-    <button data-action="live#action" data-live-action-param="save">Save</button>
+    <button data-action="live#action" data-action-name="save">Save</button>
 
 When the user clicks that button, it will attempt to call the ``save``
 action in the *child* component only, even if the ``save`` action
@@ -2944,7 +2930,7 @@ In the ``EditPost`` template, you render the
 
             <button
                 data-action="live#action"
-                data-live-action-param="save"
+                data-action-name="save"
             >Save</button>
         </form>
     </div>
@@ -3462,7 +3448,7 @@ bound to Symfony's BC policy for the moment.
 
 .. _`TwigComponent`: https://symfony.com/bundles/ux-twig-component/current/index.html
 .. _TwigComponent documentation: https://symfony.com/bundles/ux-twig-component/current/index.html
-.. _`Livewire`: https://livewire.laravel.com
+.. _`Livewire`: https://laravel-livewire.com
 .. _`Phoenix LiveView`: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html
 .. _`Twig Component`: https://symfony.com/bundles/ux-twig-component/current/index.html
 .. _`Twig Component mount documentation`: https://symfony.com/bundles/ux-twig-component/current/index.html#the-mount-method
@@ -3470,6 +3456,7 @@ bound to Symfony's BC policy for the moment.
 .. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html
 .. _`dependent form fields`: https://ux.symfony.com/live-component/demos/dependent-form-fields
 .. _StimulusBundle configured in your app: https://symfony.com/bundles/StimulusBundle/current/index.html
+.. _`localizes its URLs`: https://symfony.com/doc/current/translation/locale.html#translation-locale-url
 .. _`attributes variable`: https://symfony.com/bundles/ux-twig-component/current/index.html#component-attributes
 .. _`CollectionType`: https://symfony.com/doc/current/form/form_collections.html
 .. _`the traditional collection type`: https://symfony.com/doc/current/form/form_themes.html#fragment-naming-for-collections
@@ -3480,6 +3467,3 @@ bound to Symfony's BC policy for the moment.
 .. _`PostMount hook`: https://symfony.com/bundles/ux-twig-component/current/index.html#postmount-hook
 .. _`validation groups`: https://symfony.com/doc/current/form/validation_groups.html
 .. _morphing library: https://github.com/bigskysoftware/idiomorph
-.. _`_locale route parameter`: https://symfony.com/doc/current/translation.html#the-locale-and-the-url
-.. _`setting the locale in the request`: https://symfony.com/doc/current/translation.html#translation-locale
-.. _`Stimulus action parameter`: https://stimulus.hotwired.dev/reference/actions#action-parameters
diff --git a/src/LiveComponent/phpunit.xml.dist b/src/LiveComponent/phpunit.xml.dist
index 8798e3cc..989e6115 100644
--- a/src/LiveComponent/phpunit.xml.dist
+++ b/src/LiveComponent/phpunit.xml.dist
@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
+
 <!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
 <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit.xsd"
@@ -7,16 +8,10 @@
          failOnRisky="true"
          failOnWarning="true"
 >
-    <coverage processUncoveredFiles="true">
-        <include>
-            <directory suffix=".php">./src</directory>
-        </include>
-    </coverage>
-
     <php>
-        <ini name="error_reporting" value="-1"/>
-        <server name="KERNEL_CLASS" value="Symfony\UX\LiveComponent\Tests\Fixtures\Kernel"/>
-        <server name="DATABASE_URL" value="sqlite:///%kernel.project_dir%/var/data.db"/>
+        <ini name="error_reporting" value="-1" />
+        <server name="KERNEL_CLASS" value="Symfony\UX\LiveComponent\Tests\Fixtures\Kernel" />
+        <server name="DATABASE_URL" value="sqlite:///%kernel.project_dir%/var/data.db" />
         <env name="SHELL_VERBOSITY" value="-1"/>
         <server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0"/>
     </php>
@@ -27,6 +22,12 @@
         </testsuite>
     </testsuites>
 
+    <filter>
+        <whitelist processUncoveredFilesFromWhitelist="true">
+            <directory suffix=".php">./src</directory>
+        </whitelist>
+    </filter>
+
     <listeners>
         <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
     </listeners>
diff --git a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
index 8683214a..97c48d75 100644
--- a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
+++ b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
@@ -256,11 +256,7 @@ final class LiveComponentExtension extends Extension implements PrependExtension
             ->setArguments(['%kernel.cache_dir%/'.self::TEMPLATES_MAP_FILENAME]);
 
         $container->register('ux.live_component.twig.cache_warmer', TemplateCacheWarmer::class)
-            ->setArguments([
-                new Reference('twig.template_iterator'),
-                self::TEMPLATES_MAP_FILENAME,
-                '%kernel.secret%',
-            ])
+            ->setArguments([new Reference('twig.template_iterator'), self::TEMPLATES_MAP_FILENAME])
             ->addTag('kernel.cache_warmer');
     }
 
diff --git a/src/LiveComponent/src/Form/Type/LiveCollectionType.php b/src/LiveComponent/src/Form/Type/LiveCollectionType.php
index 5da11a16..9ac5c71b 100644
--- a/src/LiveComponent/src/Form/Type/LiveCollectionType.php
+++ b/src/LiveComponent/src/Form/Type/LiveCollectionType.php
@@ -47,8 +47,7 @@ final class LiveCollectionType extends AbstractType
 
             $attr = $view->vars['button_add']->vars['attr'];
             $attr['data-action'] ??= 'live#action';
-            $attr['data-live-action-param'] ??= 'addCollectionItem';
-            $attr['data-live-name-param'] ??= $view->vars['full_name'];
+            $attr['data-action-name'] ??= sprintf('addCollectionItem(name=%s)', $view->vars['full_name']);
             $view->vars['button_add']->vars['attr'] = $attr;
 
             array_splice($view->vars['button_add']->vars['block_prefixes'], 1, 0, 'live_collection_button_add');
@@ -86,9 +85,7 @@ final class LiveCollectionType extends AbstractType
 
                 $attr = $entryView->vars['button_delete']->vars['attr'];
                 $attr['data-action'] ??= 'live#action';
-                $attr['data-live-action-param'] ??= 'removeCollectionItem';
-                $attr['data-live-name-param'] ??= $view->vars['full_name'];
-                $attr['data-live-index-param'] ??= $k;
+                $attr['data-action-name'] ??= sprintf('removeCollectionItem(name=%s, index=%s)', $view->vars['full_name'], $k);
                 $entryView->vars['button_delete']->vars['attr'] = $attr;
 
                 array_splice($entryView->vars['button_delete']->vars['block_prefixes'], 1, 0, 'live_collection_button_delete');
diff --git a/src/LiveComponent/src/LiveComponentHydrator.php b/src/LiveComponent/src/LiveComponentHydrator.php
index d64be76b..85638731 100644
--- a/src/LiveComponent/src/LiveComponentHydrator.php
+++ b/src/LiveComponent/src/LiveComponentHydrator.php
@@ -518,7 +518,7 @@ final class LiveComponentHydrator
             }
 
             if (null !== $dateFormat) {
-                return $className::createFromFormat($dateFormat, $value) ?: throw new BadRequestHttpException(sprintf('The model path "%s" was sent invalid date data "%s" or in an invalid format. Make sure it\'s a valid date and it matches the expected format "%s".', $propertyPathForError, $value, $dateFormat));
+                return $className::createFromFormat($dateFormat, $value);
             }
 
             return new $className($value);
diff --git a/src/LiveComponent/src/Twig/TemplateCacheWarmer.php b/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
index 3c9d18ac..18b33be2 100644
--- a/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
+++ b/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
@@ -22,18 +22,15 @@ use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
  */
 final class TemplateCacheWarmer implements CacheWarmerInterface
 {
-    public function __construct(
-        private readonly \IteratorAggregate $templateIterator,
-        private readonly string $cacheFilename,
-        private readonly string $secret,
-    ) {
+    public function __construct(private \IteratorAggregate $templateIterator, private readonly string $cacheFilename)
+    {
     }
 
     public function warmUp(string $cacheDir, ?string $buildDir = null): array
     {
         $map = [];
         foreach ($this->templateIterator as $item) {
-            $map[hash('xxh128', $item.$this->secret)] = $item;
+            $map[bin2hex(random_bytes(16))] = $item;
         }
 
         (new PhpArrayAdapter($cacheDir.'/'.$this->cacheFilename, new NullAdapter()))->warmUp(['map' => $map]);
diff --git a/src/LiveComponent/src/Twig/TemplateMap.php b/src/LiveComponent/src/Twig/TemplateMap.php
index 03dd2ffd..75b8f289 100644
--- a/src/LiveComponent/src/Twig/TemplateMap.php
+++ b/src/LiveComponent/src/Twig/TemplateMap.php
@@ -28,7 +28,7 @@ final class TemplateMap
         $this->map = (new PhpArrayAdapter($cacheFile, new NullAdapter()))->getItem('map')->get();
     }
 
-    public function resolve(string $obscuredName): string
+    public function resolve(string $obscuredName)
     {
         return $this->map[$obscuredName] ?? throw new \RuntimeException(sprintf('Cannot find a template matching "%s". Cache may be corrupt.', $obscuredName));
     }
diff --git a/src/Translator/CHANGELOG.md b/src/Translator/CHANGELOG.md
index 379af30c..f0b76a2a 100644
--- a/src/Translator/CHANGELOG.md
+++ b/src/Translator/CHANGELOG.md
@@ -1,9 +1,5 @@
 # CHANGELOG
 
-## UNRELEASED
-
--   Increase version range of `intl-messageformat` to `^10.5.11`, in order tou se a faster implementation of ICU messages parsing.
-
 ## 2.13.2
 
 -   Revert "Change JavaScript package to `type: module`"
diff --git a/src/Translator/assets/package.json b/src/Translator/assets/package.json
index 53c12bbc..9e8d1f2d 100644
--- a/src/Translator/assets/package.json
+++ b/src/Translator/assets/package.json
@@ -7,14 +7,14 @@
     "types": "dist/translator_controller.d.ts",
     "symfony": {
         "importmap": {
-            "intl-messageformat": "^10.5.11",
+            "intl-messageformat": "^10.2.5",
             "@symfony/ux-translator": "path:%PACKAGE%/dist/translator_controller.js",
             "@app/translations": "path:var/translations/index.js",
             "@app/translations/configuration": "path:var/translations/configuration.js"
         }
     },
     "peerDependencies": {
-        "intl-messageformat": "^10.5.11"
+        "intl-messageformat": "^10.2.5"
     },
     "peerDependenciesMeta": {
         "intl-messageformat": {
@@ -22,6 +22,6 @@
         }
     },
     "devDependencies": {
-        "intl-messageformat": "^10.5.11"
+        "intl-messageformat": "^10.2.5"
     }
 }
diff --git a/src/Translator/assets/src/formatters/intl-formatter.ts b/src/Translator/assets/src/formatters/intl-formatter.ts
index 1aac5733..d1e898f9 100644
--- a/src/Translator/assets/src/formatters/intl-formatter.ts
+++ b/src/Translator/assets/src/formatters/intl-formatter.ts
@@ -1,4 +1,4 @@
-import { IntlMessageFormat } from 'intl-messageformat';
+import {IntlMessageFormat} from 'intl-messageformat';
 
 /**
  * @private
@@ -8,13 +8,13 @@ import { IntlMessageFormat } from 'intl-messageformat';
  * @param locale     The locale
  */
 export function formatIntl(id: string, parameters: Record<string, string | number> = {}, locale: string): string {
-    if (id === '') {
+    if (id === '' ) {
         return '';
     }
 
-    const intlMessage = new IntlMessageFormat(id, [locale.replace('_', '-')], undefined, { ignoreTag: true });
+    const intlMessage = new IntlMessageFormat(id, [locale.replace('_', '-')], undefined, {ignoreTag: true});
 
-    parameters = { ...parameters };
+    parameters = {...parameters};
 
     Object.entries(parameters).forEach(([key, value]) => {
         if (key.includes('%') || key.includes('{')) {
diff --git a/src/Turbo/CHANGELOG.md b/src/Turbo/CHANGELOG.md
index 4ece2756..73e1fb0c 100644
--- a/src/Turbo/CHANGELOG.md
+++ b/src/Turbo/CHANGELOG.md
@@ -1,10 +1,5 @@
 # CHANGELOG
 
-## 2.15.0
-
--   Add Turbo 8 support #1476
--   Fix missing `use` statement used during broadcast #1475
-
 ## 2.14.2
 
 -   Fix using old `ClassUtils` class that's not used in newer versions of Doctrine
diff --git a/src/TwigComponent/CHANGELOG.md b/src/TwigComponent/CHANGELOG.md
index f3cc7362..df794181 100644
--- a/src/TwigComponent/CHANGELOG.md
+++ b/src/TwigComponent/CHANGELOG.md
@@ -1,16 +1,11 @@
 # CHANGELOG
 
-## 2.15.0
-
--   Add the ability to render specific attributes from the `attributes` variable #1442
--   Restrict Twig 3.9 for now #1486
--   Build reproducible TemplateMap to fix possible post-deploy breakage #1497
-
 ## 2.14.0
 
 -   Make `ComponentAttributes` traversable/countable
 -   Fixed lexing some `{# twig comments #}` with HTML Twig syntax
 -   Fix various usages of deprecated Twig code
+-   Add attribute rendering system
 
 ## 2.13.0
 
diff --git a/src/TwigComponent/composer.json b/src/TwigComponent/composer.json
index 766b8a60..090ffe23 100644
--- a/src/TwigComponent/composer.json
+++ b/src/TwigComponent/composer.json
@@ -31,7 +31,7 @@
         "symfony/deprecation-contracts": "^2.2|^3.0",
         "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
         "symfony/property-access": "^5.4|^6.0|^7.0",
-        "twig/twig": "~3.8.0"
+        "twig/twig": "^2.14.7|~3.8.0"
     },
     "require-dev": {
         "symfony/console": "^5.4|^6.0|^7.0",
diff --git a/src/TwigComponent/doc/index.rst b/src/TwigComponent/doc/index.rst
index 45e3d00a..72e93491 100644
--- a/src/TwigComponent/doc/index.rst
+++ b/src/TwigComponent/doc/index.rst
@@ -306,7 +306,7 @@ want to render on the root element:
 
     <twig:Alert message="hello!" id="custom-alert-id" />
 
-To pass an array of attributes, use ``{{...}}`` spread operator syntax.
+To pass an array of attributes, use `{{...}}` spread operator syntax.
 This requires Twig 3.7.0 or higher:
 
 .. code-block:: html+twig
diff --git a/src/TwigComponent/phpunit.xml.dist b/src/TwigComponent/phpunit.xml.dist
index cd1dda80..59ac0154 100644
--- a/src/TwigComponent/phpunit.xml.dist
+++ b/src/TwigComponent/phpunit.xml.dist
@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
+
 <!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
 <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit.xsd"
@@ -7,15 +8,9 @@
          failOnRisky="true"
          failOnWarning="true"
 >
-    <coverage processUncoveredFiles="true">
-        <include>
-            <directory suffix=".php">./src</directory>
-        </include>
-    </coverage>
-
     <php>
-        <ini name="error_reporting" value="-1"/>
-        <server name="KERNEL_CLASS" value="Symfony\UX\TwigComponent\Tests\Fixtures\Kernel"/>
+        <ini name="error_reporting" value="-1" />
+        <server name="KERNEL_CLASS" value="Symfony\UX\TwigComponent\Tests\Fixtures\Kernel" />
         <server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0"/>
     </php>
 
@@ -25,6 +20,12 @@
         </testsuite>
     </testsuites>
 
+    <filter>
+        <whitelist processUncoveredFilesFromWhitelist="true">
+            <directory suffix=".php">./src</directory>
+        </whitelist>
+    </filter>
+
     <listeners>
         <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
     </listeners>
diff --git a/src/TwigComponent/src/ComponentRenderer.php b/src/TwigComponent/src/ComponentRenderer.php
index 48fdedca..d45e95c6 100644
--- a/src/TwigComponent/src/ComponentRenderer.php
+++ b/src/TwigComponent/src/ComponentRenderer.php
@@ -72,6 +72,10 @@ final class ComponentRenderer implements ComponentRendererInterface
         }
 
         try {
+            if ($this->twig::MAJOR_VERSION < 3) {
+                return $this->twig->loadTemplate($event->getTemplate(), $event->getTemplateIndex())->render($variables);
+            }
+
             return $this->twig->loadTemplate(
                 $this->twig->getTemplateClass($event->getTemplate()),
                 $event->getTemplate(),
diff --git a/ux.symfony.com/assets/controllers.json b/ux.symfony.com/assets/controllers.json
index 48507999..d37124f1 100644
--- a/ux.symfony.com/assets/controllers.json
+++ b/ux.symfony.com/assets/controllers.json
@@ -87,7 +87,7 @@
         "@symfony/ux-turbo": {
             "turbo-core": {
                 "enabled": true,
-                "fetch": "eager"
+                "fetch": "lazy"
             },
             "mercure-turbo-stream": {
                 "enabled": true,
diff --git a/ux.symfony.com/assets/styles/demos/_live-memory.scss b/ux.symfony.com/assets/styles/demos/_live-memory.scss
deleted file mode 100644
index ba03970c..00000000
--- a/ux.symfony.com/assets/styles/demos/_live-memory.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-// -----------------------------------------------------------------
-// LiveMemory demo
-// -----------------------------------------------------------------
-
-@import "live-memory/app/reset";
-@import "live-memory/app/root";
-@import "live-memory/app/html";
-
-@import "live-memory/base/header";
-
-@import "../components/Icon";
-@import "live-memory/components/Board";
-@import "live-memory/components/Card";
-@import "live-memory/components/Debug";
-@import "live-memory/components/Logo";
-@import "live-memory/components/Metric";
-@import "live-memory/components/Overlay";
-@import "live-memory/components/Score";
-@import "live-memory/components/ThemeSwitch";
-@import "live-memory/components/Timer";
-
-@import "../utilities/background";
-@import "../utilities/shadow";
-
-@import "live-memory/app/themes";
diff --git a/ux.symfony.com/assets/styles/demos/live-memory.min.css b/ux.symfony.com/assets/styles/demos/live-memory.min.css
index 747561dd..db51ecfd 100644
--- a/ux.symfony.com/assets/styles/demos/live-memory.min.css
+++ b/ux.symfony.com/assets/styles/demos/live-memory.min.css
@@ -1,992 +1,26 @@
-*, *::before, *::after {
-  box-sizing: border-box;
-}
+/** -----------------------------------------------------------------
+ * LiveMemory demo
+ * --------------------------------------------------------------- */
 
-* {
-  margin: 0;
-  padding: 0;
-}
+@import "live-memory/app/reset";
+@import "live-memory/app/root";
+@import "live-memory/app/html";
 
-body {
-  line-height: 1.5;
-  -webkit-font-smoothing: antialiased;
-}
+@import "live-memory/base/header";
 
-img, picture, video, canvas, svg {
-  display: block;
-  max-width: 100%;
-}
+@import "../components/Icon";
+@import "live-memory/components/Board";
+@import "live-memory/components/Card";
 
-input, button, textarea, select {
-  font: inherit;
-  background: none;
-  color: inherit;
-  border: 0;
-  padding: 0;
-}
+@import "live-memory/components/Debug";
+@import "live-memory/components/Logo";
+@import "live-memory/components/Metric";
+@import "live-memory/components/Overlay";
+@import "live-memory/components/Score";
+@import "live-memory/components/ThemeSwitch";
+@import "live-memory/components/Timer";
 
-#root, #__next {
-  isolation: isolate;
-}
+@import "../utilities/background";
+@import "../utilities/shadow";
 
-:root {
-  --radius: 1rem;
-  --space: 1rem;
-  --space-large: 1.5rem;
-  --font-title: system-ui, sans-serif;
-  --font-text: system-ui, sans-serif;
-  --font-code: ui-monospace, "SF Mono", SFMono-Regular, "Source Code Pro", Consolas, monospace;
-  --container-max-width: 1280px;
-}
-
-:root,
-[data-theme=light],
-[data-bs-theme=light] {
-  --color-text: #212529;
-  --color-text-rgb: 33 37 41;
-  --color-background: #fff;
-  --color-background-rgb: 255 255 255;
-  --color-border: #DEE2E6;
-}
-
-[data-theme=dark],
-[data-bs-theme=dark] {
-  --color-text: #DEE2E6;
-  --color-text-rgb: 222 226 230;
-  --color-background: #212529;
-  --color-background-rgb: 33 37 41;
-  --color-border: #1a1d20;
-}
-
-:root {
-  font-size: 100%;
-}
-
-body {
-  font-family: var(--font-text), sans-serif;
-  font-weight: 400;
-  min-height: 100vh;
-  min-width: 320px;
-  overflow-x: clip;
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  align-items: stretch;
-  background: var(--color-background);
-  color: var(--color-text);
-  transition: background 500ms linear, color 500ms ease-in-out;
-}
-
-::selection {
-  background: var(--color-text);
-  color: var(--color-background);
-  -webkit-text-fill-color: currentColor;
-}
-
-a {
-  text-decoration: none;
-  text-underline-offset: 0.25ex;
-  color: inherit;
-}
-
-main {
-  flex: 1 1 auto;
-  width: 100%;
-  display: grid;
-}
-
-.wrapper {
-  padding: 0 1rem;
-  max-inline-size: var(--container-max-width);
-  margin-inline: auto;
-}
-
-.wrapper--full {
-  max-inline-size: 100%;
-}
-
-.Header {
-  padding: 1rem;
-  width: 100%;
-  max-inline-size: var(--container-max-width);
-  align-items: center;
-  margin-inline: auto;
-  display: grid;
-  grid-template-areas: "a logo b";
-  grid-template-columns: 1fr auto 1fr;
-}
-
-.HeaderLogo {
-  width: min(25vw, 220px);
-  mix-blend-mode: hard-light;
-  order: 2;
-}
-
-.HeaderLinks {
-  grid-area: b;
-  display: flex;
-  justify-content: flex-end;
-  flex-direction: row;
-  align-items: center;
-  gap: 1rem;
-}
-
-.HeaderButton {
-  width: 2.25rem;
-  height: 2.25rem;
-  border-radius: 12%;
-  opacity: 0.75;
-  display: grid;
-  place-content: center;
-  align-items: center;
-  background: transparent;
-  border: 1px solid transparent;
-  transition: 250ms all ease-in-out;
-}
-
-.HeaderButton:hover {
-  opacity: 0.85;
-  background: var(--color-background);
-  border: 1px solid var(--color-border);
-}
-
-.Icon {
-  display: inline-flex;
-  justify-content: center;
-  align-items: center;
-  height: 1em;
-  width: 1em;
-  color: var(--icon-color);
-  vertical-align: -0.125em;
-}
-.Icon svg {
-  fill: currentColor;
-  height: 100%;
-  width: 100%;
-}
-
-body::before {
-  position: fixed;
-  inset: 0;
-  content: "";
-  background: var(--background-cover-url);
-  background-attachment: fixed;
-  background-size: cover;
-  background-blend-mode: luminosity;
-  filter: blur(10px);
-  transform: scale(1.1);
-  opacity: 0.125;
-  transition: background 1s ease-in-out;
-}
-
-.LiveMemory {
-  padding: 1rem;
-  border: 1px solid var(--color-border);
-  border-radius: 1rem;
-  background: rgb(var(--color-background-rgb)/75%);
-  align-self: center;
-}
-
-.LiveMemory-Board {
-  display: flex;
-  flex-direction: column;
-  gap: 1rem;
-  position: relative;
-}
-
-.LiveMemory-Main {
-  display: grid;
-  align-items: center;
-  position: relative;
-  overflow: hidden;
-  border-radius: 1vmin;
-  background: rgb(var(--color-background-rgb)/50%);
-  border: 1px solid var(--color-border);
-  width: 100%;
-  aspect-ratio: 1.6 auto;
-}
-
-.LiveMemory-Background {
-  position: absolute;
-  inset: 0;
-}
-
-.LiveMemory-Tableau {
-  position: relative;
-  justify-self: normal;
-  align-self: normal;
-  display: grid;
-  padding: 1vmin;
-}
-
-.LiveMemory-Cards {
-  --cards-x: max(var(--deck-x), var(--deck-y));
-  --cards-y: min(var(--deck-x), var(--deck-y));
-  grid-template-columns: repeat(var(--cards-x), 1fr);
-  grid-template-rows: repeat(var(--cards-y), 1fr);
-  display: grid;
-  justify-self: center;
-  aspect-ratio: var(--cards-x)/var(--cards-y);
-  --card-space: calc(max(1vmin));
-}
-
-.LiveMemory-CardSlot {
-  padding: max(0.75rem, 0.5 * var(--card-space));
-  position: relative;
-  display: grid;
-  align-content: center;
-}
-
-@media (orientation: landscape) {
-  .LiveMemory-Cards {
-    height: calc(100% - 2 * var(--card-space));
-    margin-block: var(--card-space);
-    max-width: 100%;
-  }
-}
-@media (orientation: portrait) {
-  .LiveMemory-Main {
-    aspect-ratio: 1/1.6 auto;
-  }
-  .LiveMemory-Cards {
-    width: calc(100% - 2 * var(--card-space));
-    padding-inline: var(--card-space);
-    align-self: center;
-    --cards-x: min(var(--deck-x), var(--deck-y));
-    --cards-y: max(var(--deck-x), var(--deck-y));
-  }
-}
-.LiveMemory-Scores {
-  position: absolute;
-  inset: 0;
-}
-
-.LiveMemory-Footer {
-  display: grid;
-  grid-template-columns: 1fr 1fr 1fr;
-  gap: 1rem;
-  align-items: center;
-  justify-content: space-between;
-  line-height: 0.5rem;
-  word-spacing: -0.125rem;
-  mix-blend-mode: color-burn;
-  font-size: 0.65rem;
-  font-weight: lighter;
-  font-family: monospace;
-  text-transform: uppercase;
-  --footer-color: var(--color-text);
-  --footer-color-subtle: var(--color-text);
-}
-[data-bs-theme=dark] .LiveMemory-Footer {
-  mix-blend-mode: overlay;
-}
-
-.LiveMemory-Footer span:first-child {
-  text-align: left;
-}
-
-.LiveMemory-Footer span:last-child {
-  text-align: right;
-}
-
-.LiveMemory-Footer > span {
-  text-align: center;
-}
-
-.LiveMemory-Footer a {
-  font-weight: lighter;
-  text-align: center;
-  color: var(--footer-color);
-}
-
-.LiveMemory-Footer a:hover {
-  color: inherit;
-  opacity: 1;
-  text-decoration: underline;
-}
-
-.LiveMemory-Card {
-  --card-radius: max(8%, 1.5vmin);
-  --card-delta: var(--card-delta, 0);
-  aspect-ratio: 1/1;
-  transform-style: preserve-3d;
-  transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
-  cursor: pointer;
-  max-width: 12rem;
-}
-
-.LiveMemory-Card--rotating {
-  animation: rotate 1.5s forwards infinite ease-in-out;
-}
-
-.LiveMemory-Card__back {
-  padding: 0;
-  border: 0;
-}
-
-.LiveMemory-Card__front,
-.LiveMemory-Card__back {
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  backface-visibility: hidden;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: var(--card-radius, 1vmin);
-  overflow: hidden;
-}
-
-.LiveMemory-Card__img {
-  width: 100%;
-  height: auto;
-  transition: transform 0.75s ease-in-out, filter 0.75s ease-in-out;
-}
-
-.LiveMemory-Card:not(.LiveMemory-Card--flipped):hover {
-  --delta: calc(5 - var(--card-delta, 0));
-  transform: scale(1.025) rotate3d(calc(0.05 * var(--delta)), 0.01, -0.01, calc(1deg * var(--delta)));
-  filter: drop-shadow(0.2rem 0.2rem 1rem rgba(var(--live-memory-color-rgb), 0.25));
-}
-
-.LiveMemory-Card:hover .LiveMemory-Card__back_img {
-  transform: scale(1.01);
-  transition: all 0.75s ease-in-out;
-}
-
-.LiveMemory-Card__front {
-  transform: rotateY(180deg);
-}
-
-.LiveMemory-Card--flipped {
-  transform: rotateY(180deg);
-}
-
-.LiveMemory-Card--flipped .LiveMemory-Card__img {
-  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
-}
-
-.LiveMemory-Card--matched {
-  animation: matched 1500ms forwards ease-in-out;
-  animation-iteration-count: 1;
-  animation-fill-mode: both;
-  scale: 0.95;
-}
-
-.LiveMemory-Card--matched .LiveMemory-Card__front {
-  border: 4px solid color-mix(in oklab, var(--live-memory-color) 50%, var(--color-text));
-}
-
-.LiveMemory-Card--matched:not(.LiveMemory-Card--paired) {
-  animation-fill-mode: forwards;
-}
-
-@keyframes matched {
-  0% {
-    rotate: -720deg;
-    scale: 1;
-  }
-  50% {
-    scale: 0.55;
-    filter: motionBlur(10px, 90deg);
-  }
-  100% {
-    rotate: 0;
-    scale: 0.95;
-  }
-}
-.LiveMemory-CardDots {
-  position: absolute;
-  inset: 10%;
-  display: grid;
-  grid-template: repeat(3, 1fr)/repeat(3, 1fr);
-  rotate: calc(18deg * var(--slot-delta, 0));
-}
-
-.LiveMemory-CardDots span {
-  animation: live-memory-match 1.5s ease-out;
-  rotate: calc(var(--i) * 57deg);
-  width: calc(var(--i) * 0.1rem);
-  height: calc(var(--i) * 0.1rem);
-  border-radius: 50%;
-  mix-blend-mode: overlay;
-  animation-iteration-count: 1;
-  animation-fill-mode: forwards;
-  z-index: 0;
-}
-
-@keyframes live-memory-match {
-  0% {
-    opacity: 0;
-    transform: translateX(0);
-    scale: 0.5;
-    background: var(--live-memory-color);
-  }
-  5% {
-    scale: 1;
-  }
-  20% {
-    opacity: 0.75;
-    transform: translateX(2vmin);
-    scale: calc(1 + var(--i) * 0.1);
-  }
-  50% {
-    opacity: 0.1;
-    background: #fff;
-  }
-  100% {
-    scale: calc(2 + var(--i) * 0.1);
-    opacity: 0;
-    transform: translateX(calc(var(--i) * 1vmin + 2vmin));
-    background: #fff;
-  }
-}
-.LiveMemory-Debug {
-  position: absolute;
-  top: 3.5rem;
-  left: 0;
-  display: flex;
-  gap: 0.5rem;
-  align-items: center;
-  padding: 0.5rem;
-  font-size: 0.65rem;
-  font-weight: lighter;
-  opacity: 0.01;
-  transition: opacity 250ms;
-}
-.LiveMemory-Debug:hover {
-  opacity: 1;
-}
-
-.LiveMemory-Debug button {
-  font-family: var(--font-code);
-  padding: 0.1rem;
-  color: var(--color-text);
-  cursor: pointer;
-  opacity: 0.75;
-}
-.LiveMemory-Debug button:hover {
-  opacity: 1;
-}
-
-.LiveMemory-Logo {
-  --bottom: 0;
-  max-width: 320px;
-  transition: all 150ms ease-in-out;
-  transform: scale(1);
-  align-items: center;
-  display: flex;
-  flex-direction: column;
-  transform-style: preserve-3d;
-  gap: 0.25rem;
-  opacity: 0.75;
-}
-.LiveMemory-Logo:hover {
-  opacity: 0.95;
-}
-
-.LiveMemory-Logo-Live {
-  width: 58%;
-  height: auto;
-  position: relative;
-}
-.LiveMemory-Logo-Live path {
-  transform: rotateY(0deg);
-  transform-origin: center;
-  transition: transform 900ms ease-in-out;
-  transform-style: preserve-3d;
-  transform-box: fill-box;
-}
-
-.LiveMemory-Logo:hover .LiveMemory-Logo-Live path {
-  transform: rotateY(720deg);
-  transition-delay: calc(var(--delay, 200ms) * var(--idx, 0));
-}
-
-.LiveMemory-Logo-Memory {
-  color: var(--color-text);
-  width: 95%;
-  transform: scale(0.9);
-  height: auto;
-  transition: color 250ms linear, transform 900ms ease-in-out;
-  mix-blend-mode: luminosity;
-}
-
-.LiveMemory-Logo:hover .LiveMemory-Logo-Memory {
-  transform: scale(0.95);
-  transition-delay: 400ms;
-}
-
-.LiveMemory-Metrics dl {
-  --rows: 4;
-  display: grid;
-  gap: 1rem;
-  grid-template-columns: repeat(var(--rows), 1fr);
-  margin: 0;
-}
-
-@media screen and (max-width: 480px) {
-  .LiveMemory-Metrics dl {
-    --rows: 2;
-  }
-}
-.LiveMemory-Metric {
-  --metric-opacity: 1;
-  display: flex;
-  justify-content: space-between;
-  align-content: center;
-  justify-items: center;
-  align-items: center;
-  padding: 0.5rem 0.75rem;
-  border-radius: 0.5rem;
-  font-size: 0.8rem;
-  background: rgb(var(--color-background-rgb)/50%);
-  border: 1px solid var(--color-border);
-  font-family: monospace;
-  text-transform: uppercase;
-  color: var(--color-text);
-}
-
-.LiveMemory-Metric dt {
-  font-weight: lighter;
-  display: flex;
-  align-items: center;
-  gap: 0.65em;
-  opacity: var(--metric-opacity);
-}
-
-.LiveMemory-Metric dt svg {
-  aspect-ratio: 1/1;
-  height: 1rem;
-  display: block;
-  width: auto;
-}
-
-.LiveMemory-Metric dt span {
-  opacity: 0.75;
-}
-
-.LiveMemory-Metric dd {
-  font-size: 1.1em;
-  font-family: var(--font-code);
-  font-weight: 600;
-  margin: 0;
-  opacity: var(--metric-opacity);
-}
-
-.LiveMemory-Metric dd small {
-  font-weight: 300;
-  font-size: 100%;
-}
-
-.LiveMemory-Overlay {
-  position: absolute;
-  inset: 0;
-  height: 105%;
-  background: rgb(var(--color-background-rgb)/50%);
-  backdrop-filter: blur(1rem);
-  visibility: hidden;
-}
-
-.LiveMemory-Overlay.on {
-  visibility: visible;
-}
-
-.LiveMemory-Overlay:before {
-  content: "";
-  position: absolute;
-  inset: 0;
-  background: rgba(0, 0, 0, 0.8);
-  transition: all 2000ms ease-out;
-  transition-duration: 0s;
-  opacity: 0.2;
-}
-
-.LiveMemory-Overlay.on:before {
-  opacity: 0.95;
-  transition-duration: 2s;
-}
-
-.LiveMemory-Overlay div {
-  height: 20%;
-  background: #0A0A0A;
-  width: 100%;
-  position: relative;
-  opacity: 75%;
-  animation-delay: 0ms;
-  animation-duration: 0ms;
-  animation: none;
-  transition-delay: 0ms;
-}
-
-.LiveMemory-Overlay.on div {
-  animation: slide 600ms linear;
-  transform: scaleX(1) scaleY(1.01);
-  transform-origin: left;
-  transition-delay: 750ms;
-  animation-delay: 0ms;
-  animation-fill-mode: forwards;
-}
-
-.LiveMemory-Overlay.on div:nth-child(2n) {
-  transform-origin: right;
-  transition-delay: 0ms;
-  animation-delay: 0ms;
-  animation-duration: 900ms;
-}
-
-@keyframes slide {
-  from {
-    transform: scaleX(0);
-  }
-  to {
-    transform: scaleX(1);
-  }
-}
-.LiveMemory-Score {
-  position: absolute;
-  inset: 0;
-  display: grid;
-  align-items: stretch;
-  justify-items: stretch;
-  border-radius: 1vmin;
-  overflow: hidden;
-  visibility: hidden;
-}
-
-.LiveMemory-Score.on {
-  visibility: visible;
-  z-index: 1;
-}
-
-.LiveMemory-Score.on .result {
-  z-index: 1;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: space-evenly;
-}
-
-.LiveMemory-Score .trophy {
-  height: 20vmin;
-  position: relative;
-  min-height: 130px;
-  aspect-ratio: 1;
-}
-
-.LiveMemory-Score .trophy img {
-  max-height: 100%;
-  max-width: 100%;
-  animation: live-memory-trophy steps(8) 2s infinite alternate-reverse;
-  display: block;
-  object-fit: cover;
-  min-width: 1rem;
-  min-height: 1rem;
-}
-
-.LiveMemory-Score .trophy.gameover img {
-  filter: grayscale(1) opacity(0.2);
-}
-
-@keyframes live-memory-trophy {
-  0% {
-    transform: rotateY(0deg);
-  }
-  49% {
-    transform: rotateY(80deg);
-  }
-  50% {
-    transform: rotateY(100deg);
-  }
-  100% {
-    transform: rotateY(180deg);
-  }
-}
-.LiveMemory-Score {
-  --font-size: .9rem;
-}
-
-.LiveMemory-Score .title {
-  font-family: var(--font-title);
-  font-size: clamp(1rem, 2vw, 2rem);
-  color: rgba(255, 255, 255, 0.75);
-}
-
-.LiveMemory-Score .text {
-  font-family: monospace;
-  font-size: var(--font-size);
-  color: #fff;
-  letter-spacing: 2px;
-  text-transform: uppercase;
-}
-
-.LiveMemory-ScoreRow .typing {
-  --duration: .5s;
-  --wait: 0;
-  --chars: 40;
-  width: 0;
-  animation: live-memory-typing steps(var(--chars)) forwards;
-  animation-duration: var(--duration);
-  animation-delay: calc(var(--wait) * var(--duration));
-  text-overflow: clip;
-  display: inline-block;
-  white-space: nowrap;
-  overflow: clip;
-}
-
-.LiveMemory-ScoreRow.total {
-  margin-block-start: 1vmin;
-}
-
-.LiveMemory-ScoreRow .dots {
-  opacity: 0.25;
-}
-
-@keyframes live-memory-typing {
-  0% {
-    width: 0;
-  }
-  100% {
-    width: 100%;
-  }
-}
-.LiveMemory-Score button {
-  border: 0;
-  background: 0;
-  opacity: 0.75;
-  font-size: 1.1rem;
-  animation: steps(3) live-memory-button-blinking infinite alternate-reverse;
-  cursor: pointer;
-  color: #ffffff;
-}
-.LiveMemory-Score button::after {
-  position: absolute;
-  inset: 0;
-  content: " ";
-  display: block;
-  background: none;
-  z-index: 2;
-}
-
-.LiveMemory-Score button:hover {
-  opacity: 1;
-  animation-play-state: paused;
-}
-
-@keyframes live-memory-button-blinking {
-  70% {
-    opacity: 1;
-  }
-  100% {
-    opacity: 0;
-  }
-}
-.LiveMemory-ThemeSwitch {
-  position: relative;
-  transform-style: preserve-3d;
-  cursor: pointer;
-  border: 0;
-  padding: 0;
-  width: 2.25rem;
-  height: 2.25rem;
-  border-radius: 12%;
-  transition: transform 250ms linear;
-}
-[data-bs-theme=dark] .LiveMemory-ThemeSwitch {
-  transform: rotateY(180deg);
-}
-.LiveMemory-ThemeSwitch[data-switch=dark] {
-  transform: rotateY(180deg);
-}
-.LiveMemory-ThemeSwitch[data-switch=light] {
-  transform: rotateY(0deg);
-}
-
-.LiveMemory-ThemeSwitch-Mode {
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  display: grid;
-  place-content: center;
-  align-items: center;
-  backface-visibility: hidden;
-  color: var(--color-text);
-  background: rgb(var(--color-background-rgb)/50%);
-  border-radius: 4px;
-  opacity: 0.5;
-}
-
-.LiveMemory-ThemeSwitch:hover .LiveMemory-ThemeSwitch-Mode {
-  opacity: 1;
-  transition: 250ms;
-  background: rgb(var(--color-background-rgb)/100%);
-}
-
-.LiveMemory-ThemeSwitch-Dark {
-  transform: rotateY(180deg);
-}
-.LiveMemory-ThemeSwitch-Dark:hover {
-  background: var(--color-background);
-  border: var(--color-border);
-}
-
-.LiveMemory-ThemeSwitch-Light {
-  transform: rotateY(0deg);
-}
-.LiveMemory-ThemeSwitch-Light:hover {
-  background: var(--color-background);
-  border: var(--color-border);
-}
-
-.LiveMemory-Timer {
-  display: flex;
-  flex-direction: row;
-}
-
-.LiveMemory-Timer-Display {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-
-.LiveMemory-Timer-Separator {
-  display: grid;
-  place-content: center;
-}
-
-.LiveMemory-Timer-Digit {
-  overflow: hidden;
-  position: relative;
-  display: block;
-  height: 1rem;
-}
-
-.LiveMemory-Timer-Digit ul {
-  list-style: none;
-  margin: 0;
-  padding: 0;
-  display: flex;
-  flex-direction: column;
-  animation-play-state: var(--play-state);
-  animation-name: timer-tick;
-  animation-timing-function: steps(var(--steps), end);
-  animation-duration: calc(var(--duration) * 1s);
-  animation-iteration-count: var(--iterations);
-  animation-delay: var(--delay);
-  animation-fill-mode: forwards;
-  align-items: stretch;
-  justify-content: flex-end;
-}
-
-.LiveMemory-Timer-Digit li {
-  margin: 0;
-  display: block;
-  width: 1ch;
-  line-height: 1.25;
-}
-
-@keyframes timer-tick {
-  from {
-    transform: translateY(0%);
-  }
-  to {
-    transform: translateY(-100%);
-  }
-}
-.LiveMemory-Timer--blinking {
-  color: color-mix(in oklab, var(--live-memory-color) 50%, var(--color-text));
-  animation: live-memory-timer-blinking 1s infinite;
-}
-
-@keyframes live-memory-timer-blinking {
-  70% {
-    opacity: 1;
-    scale: 1.1;
-  }
-  100% {
-    opacity: 0;
-    scale: 1;
-  }
-}
-.background-glass {
-  --glass-opacity: .75;
-  --glass-color: 255 255 255;
-  background: rgba(var(--glass-color)/var(--glass-opacity));
-}
-[data-bs-theme=dark] .background-glass {
-  --glass-color: 0 0 0;
-}
-
-.background-cover {
-  position: absolute;
-  inset: 0;
-  width: 100%;
-  height: 100%;
-}
-
-.background-cover img {
-  width: 100%;
-  height: 100%;
-  object-fit: cover;
-  filter: blur(var(--blur, 2px));
-  opacity: var(--opacity, 0.15);
-}
-
-.shadow-blur {
-  position: relative;
-}
-.shadow-blur:after {
-  position: absolute;
-  content: "";
-  z-index: -1;
-  background: var(--gradient);
-  inset: 3rem;
-  border-radius: 3rem;
-  bottom: var(--shadow-bottom, 0);
-  filter: blur(3rem);
-}
-
-.shadow-blur--rainbow {
-  --gradient: linear-gradient(113.84deg, #D65831 0%, #D2D631 36.52%, #31D673 71.83%, #3aa3ff 100%) ;
-}
-
-:root {
-  --live-memory-gradient-blue: linear-gradient(90deg, #148f7d 0%, #79eaf8 50%, #237de0 100%);
-  --live-memory-gradient-green: linear-gradient(113.84deg, #518c1c 0%, #0b592b 71.83%, #247939 100%);
-  --live-memory-gradient-orange: radial-gradient(circle, rgb(225, 97, 58) 0%, rgb(195, 168, 34) 50%, rgb(252, 118, 20) 100%);
-  --live-memory-gradient-pink: radial-gradient(circle, rgb(189, 58, 225) 0%, rgb(39, 45, 115) 50%, rgb(197, 82, 201) 100%);
-  --live-memory-blue: #237de0;
-  --live-memory-blue-rgb: 35, 125, 224;
-  --live-memory-green: #518c1c;
-  --live-memory-green-rgb: 81, 140, 28;
-  --live-memory-orange: #FC7614;
-  --live-memory-orange-rgb: 252, 118, 20;
-  --live-memory-pink: #bd3ae1;
-  --live-memory-pink-rgb: 189, 58, 225;
-}
-
-[data-live-memory-theme=blue] {
-  --live-memory-color-rgb: var(--live-memory-blue-rgb);
-  --live-memory-color: var(--live-memory-blue);
-  --live-memory-gradient: var(--live-memory-gradient-blue);
-}
-
-[data-live-memory-theme=green] {
-  --live-memory-color: var(--live-memory-green);
-  --live-memory-color-rgb: var(--live-memory-green-rgb);
-  --live-memory-gradient: var(--live-memory-gradient-green);
-}
-
-[data-live-memory-theme=orange] {
-  --live-memory-color: var(--live-memory-orange);
-  --live-memory-color-rgb: var(--live-memory-orange-rgb);
-  --live-memory-gradient: var(--live-memory-gradient-orange);
-}
-
-[data-live-memory-theme=pink] {
-  --live-memory-color: var(--live-memory-pink);
-  --live-memory-color-rgb: var(--live-memory-pink-rgb);
-  --live-memory-gradient: var(--live-memory-gradient-pink);
-}
+@import "live-memory/app/themes";
diff --git a/ux.symfony.com/assets/styles/demos/live-memory/components/_Board.scss b/ux.symfony.com/assets/styles/demos/live-memory/components/_Board.scss
index 7be9f4a5..f0175923 100644
--- a/ux.symfony.com/assets/styles/demos/live-memory/components/_Board.scss
+++ b/ux.symfony.com/assets/styles/demos/live-memory/components/_Board.scss
@@ -110,9 +110,6 @@ body::before {
   line-height: .5rem;
   word-spacing: -.125rem;
   mix-blend-mode: color-burn;
-  [data-bs-theme="dark"] & {
-    mix-blend-mode: overlay;
-  }
   font-size: .65rem;
   font-weight: lighter;
   font-family: monospace;
@@ -120,6 +117,11 @@ body::before {
   --footer-color: var(--color-text);
   --footer-color-subtle: var(--color-text);
 }
+[data-bs-theme="dark"] .LiveMemory-Footer {
+  mix-blend-mode: overlay;
+}
+
+
 .LiveMemory-Footer span:first-child {
   text-align: left;
 }
diff --git a/ux.symfony.com/assets/styles/demos/live-memory/components/_Debug.scss b/ux.symfony.com/assets/styles/demos/live-memory/components/_Debug.scss
index 259b1a3f..15a564a6 100644
--- a/ux.symfony.com/assets/styles/demos/live-memory/components/_Debug.scss
+++ b/ux.symfony.com/assets/styles/demos/live-memory/components/_Debug.scss
@@ -11,9 +11,9 @@
   font-weight: lighter;
   opacity: 0.01;
   transition: opacity 250ms;
-  &:hover {
-    opacity: 1;
-  }
+}
+.LiveMemory-Debug:hover {
+  opacity: 1;
 }
 
 .LiveMemory-Debug button {
@@ -22,7 +22,4 @@
   color: var(--color-text);
   cursor: pointer;
   opacity: .75;
-  &:hover {
-    opacity: 1;
-  }
 }
diff --git a/ux.symfony.com/assets/styles/demos/live-memory/components/_Logo.scss b/ux.symfony.com/assets/styles/demos/live-memory/components/_Logo.scss
index abbf1224..01535af4 100644
--- a/ux.symfony.com/assets/styles/demos/live-memory/components/_Logo.scss
+++ b/ux.symfony.com/assets/styles/demos/live-memory/components/_Logo.scss
@@ -13,9 +13,9 @@
   transform-style: preserve-3d;
   gap: 0.25rem;
   opacity: .75;
-  &:hover {
-    opacity: .95;
-  }
+}
+.LiveMemory-Logo:hover {
+  opacity: .95;
 }
 
 .LiveMemory-Logo-Live {
diff --git a/ux.symfony.com/assets/styles/demos/live-memory/components/_Score.scss b/ux.symfony.com/assets/styles/demos/live-memory/components/_Score.scss
index bdbc685d..dd5db0ce 100644
--- a/ux.symfony.com/assets/styles/demos/live-memory/components/_Score.scss
+++ b/ux.symfony.com/assets/styles/demos/live-memory/components/_Score.scss
@@ -119,8 +119,8 @@
   animation: steps(3) live-memory-button-blinking infinite alternate-reverse;
   cursor: pointer;
   color: #ffffff;
-
-  &::after {
+}
+.LiveMemory-Score button::after {
     position: absolute;
     inset: 0;
     content: " ";
@@ -128,7 +128,6 @@
     background: none;
     z-index: 2;
   }
-}
 
 .LiveMemory-Score button:hover {
   opacity: 1;
diff --git a/ux.symfony.com/assets/styles/demos/live-memory/components/_ThemeSwitch.scss b/ux.symfony.com/assets/styles/demos/live-memory/components/_ThemeSwitch.scss
index 402acbc6..713fabeb 100644
--- a/ux.symfony.com/assets/styles/demos/live-memory/components/_ThemeSwitch.scss
+++ b/ux.symfony.com/assets/styles/demos/live-memory/components/_ThemeSwitch.scss
@@ -12,19 +12,16 @@
   height: 2.25rem;
   border-radius: 12%;
   transition: transform 250ms linear;
-
-  [data-bs-theme="dark"] & {
-    transform: rotateY(180deg);
-  }
-
-  &[data-switch="dark"] {
+}
+[data-bs-theme="dark"] .LiveMemory-ThemeSwitch {
+  transform: rotateY(180deg);
+}
+.LiveMemory-ThemeSwitch[data-switch="dark"] {
     transform: rotateY(180deg);
-  }
-
-  &[data-switch="light"] {
+}
+.LiveMemory-ThemeSwitch[data-switch="light"] {
     transform: rotateY(0deg);
   }
-}
 
 .LiveMemory-ThemeSwitch-Mode {
   position: absolute;
@@ -48,19 +45,18 @@
   background: rgb(var(--color-background-rgb) / 100%);
 }
 
-
 .LiveMemory-ThemeSwitch-Dark {
   transform: rotateY(180deg);
-  &:hover {
+}
+.LiveMemory-ThemeSwitch-Dark:hover {
     background: var(--color-background);
     border: var(--color-border);
-  }
 }
 
 .LiveMemory-ThemeSwitch-Light {
   transform: rotateY(0deg);
-  &:hover {
+}
+.LiveMemory-ThemeSwitch-Light:hover {
     background: var(--color-background);
     border: var(--color-border);
-  }
 }
diff --git a/ux.symfony.com/assets/styles/vendor/_tom-select.scss b/ux.symfony.com/assets/styles/vendor/_tom-select.scss
index 04c110db..2d6d59bc 100644
--- a/ux.symfony.com/assets/styles/vendor/_tom-select.scss
+++ b/ux.symfony.com/assets/styles/vendor/_tom-select.scss
@@ -1,5 +1,5 @@
 .ts-dropdown, .ts-control, .ts-control input {
-  color: var(--bs-body-color) !important;
+  color: var(--bs-body-color);
 }
 
 .ts-dropdown, .ts-dropdown.form-control, .ts-dropdown.form-select {
diff --git a/ux.symfony.com/composer.lock b/ux.symfony.com/composer.lock
index 0f7cc0d9..7ea39b6f 100644
--- a/ux.symfony.com/composer.lock
+++ b/ux.symfony.com/composer.lock
@@ -517,16 +517,16 @@
         },
         {
             "name": "doctrine/dbal",
-            "version": "3.8.2",
+            "version": "3.8.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "a19a1d05ca211f41089dffcc387733a6875196cb"
+                "reference": "c9ea252cdce4da324ede3d6c5913dd89f769afd2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/a19a1d05ca211f41089dffcc387733a6875196cb",
-                "reference": "a19a1d05ca211f41089dffcc387733a6875196cb",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/c9ea252cdce4da324ede3d6c5913dd89f769afd2",
+                "reference": "c9ea252cdce4da324ede3d6c5913dd89f769afd2",
                 "shasum": ""
             },
             "require": {
@@ -610,7 +610,7 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/3.8.2"
+                "source": "https://github.com/doctrine/dbal/tree/3.8.1"
             },
             "funding": [
                 {
@@ -626,7 +626,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-12T18:36:36+00:00"
+            "time": "2024-02-03T17:33:49+00:00"
         },
         {
             "name": "doctrine/deprecations",
@@ -677,16 +677,16 @@
         },
         {
             "name": "doctrine/doctrine-bundle",
-            "version": "2.11.3",
+            "version": "2.11.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/DoctrineBundle.git",
-                "reference": "492725310ae9a1b5b20d6ae09fb5ae6404616e68"
+                "reference": "4089f1424b724786c062aea50aae5f773449b94b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/492725310ae9a1b5b20d6ae09fb5ae6404616e68",
-                "reference": "492725310ae9a1b5b20d6ae09fb5ae6404616e68",
+                "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/4089f1424b724786c062aea50aae5f773449b94b",
+                "reference": "4089f1424b724786c062aea50aae5f773449b94b",
                 "shasum": ""
             },
             "require": {
@@ -707,18 +707,18 @@
             },
             "conflict": {
                 "doctrine/annotations": ">=3.0",
-                "doctrine/orm": "<2.17 || >=4.0",
+                "doctrine/orm": "<2.14 || >=4.0",
                 "twig/twig": "<1.34 || >=2.0 <2.4"
             },
             "require-dev": {
                 "doctrine/annotations": "^1 || ^2",
                 "doctrine/coding-standard": "^12",
                 "doctrine/deprecations": "^1.0",
-                "doctrine/orm": "^2.17 || ^3.0",
+                "doctrine/orm": "^2.14 || ^3.0",
                 "friendsofphp/proxy-manager-lts": "^1.0",
-                "phpunit/phpunit": "^9.5.26",
+                "phpunit/phpunit": "^9.5.26 || ^10.0",
                 "psalm/plugin-phpunit": "^0.18.4",
-                "psalm/plugin-symfony": "^5",
+                "psalm/plugin-symfony": "^4",
                 "psr/log": "^1.1.4 || ^2.0 || ^3.0",
                 "symfony/phpunit-bridge": "^6.1 || ^7.0",
                 "symfony/property-info": "^5.4 || ^6.0 || ^7.0",
@@ -731,7 +731,7 @@
                 "symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0",
                 "symfony/yaml": "^5.4 || ^6.0 || ^7.0",
                 "twig/twig": "^1.34 || ^2.12 || ^3.0",
-                "vimeo/psalm": "^5.15"
+                "vimeo/psalm": "^4.30"
             },
             "suggest": {
                 "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.",
@@ -776,7 +776,7 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/DoctrineBundle/issues",
-                "source": "https://github.com/doctrine/DoctrineBundle/tree/2.11.3"
+                "source": "https://github.com/doctrine/DoctrineBundle/tree/2.11.1"
             },
             "funding": [
                 {
@@ -792,7 +792,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-10T20:56:20+00:00"
+            "time": "2023-11-15T20:01:50+00:00"
         },
         {
             "name": "doctrine/doctrine-migrations-bundle",
@@ -978,16 +978,16 @@
         },
         {
             "name": "doctrine/inflector",
-            "version": "2.0.10",
+            "version": "2.0.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/inflector.git",
-                "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc"
+                "reference": "2930cd5ef353871c821d5c43ed030d39ac8cfe65"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc",
-                "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc",
+                "url": "https://api.github.com/repos/doctrine/inflector/zipball/2930cd5ef353871c821d5c43ed030d39ac8cfe65",
+                "reference": "2930cd5ef353871c821d5c43ed030d39ac8cfe65",
                 "shasum": ""
             },
             "require": {
@@ -1049,7 +1049,7 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/inflector/issues",
-                "source": "https://github.com/doctrine/inflector/tree/2.0.10"
+                "source": "https://github.com/doctrine/inflector/tree/2.0.9"
             },
             "funding": [
                 {
@@ -1065,7 +1065,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-18T20:23:39+00:00"
+            "time": "2024-01-15T18:05:13+00:00"
         },
         {
             "name": "doctrine/instantiator",
@@ -6835,12 +6835,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/stimulus-bundle.git",
-                "reference": "c113ab8e92c6b14659f6be4e9bef5c1f4c4a000e"
+                "reference": "da6b4a4f95a91390d38151ef67c77901a1210646"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/c113ab8e92c6b14659f6be4e9bef5c1f4c4a000e",
-                "reference": "c113ab8e92c6b14659f6be4e9bef5c1f4c4a000e",
+                "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/da6b4a4f95a91390d38151ef67c77901a1210646",
+                "reference": "da6b4a4f95a91390d38151ef67c77901a1210646",
                 "shasum": ""
             },
             "require": {
@@ -6850,7 +6850,7 @@
                 "symfony/deprecation-contracts": "^2.0|^3.0",
                 "symfony/finder": "^5.4|^6.0|^7.0",
                 "symfony/http-kernel": "^5.4|^6.0|^7.0",
-                "twig/twig": "^2.15.3|~3.8.0"
+                "twig/twig": "^2.15.3|^3.4.3"
             },
             "require-dev": {
                 "symfony/asset-mapper": "^6.3|^7.0",
@@ -6881,7 +6881,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/stimulus-bundle/tree/v2.15.0"
+                "source": "https://github.com/symfony/stimulus-bundle/tree/v2.14.1"
             },
             "funding": [
                 {
@@ -6897,7 +6897,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-14T16:26:57+00:00"
+            "time": "2024-02-03T01:11:47+00:00"
         },
         {
             "name": "symfony/stopwatch",
@@ -7493,18 +7493,17 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/ux-autocomplete.git",
-                "reference": "6f900c939b284ebcb743b63f324591aef3774a76"
+                "reference": "0966e60072be7a5b5cb433675254c7e9e4c93517"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/ux-autocomplete/zipball/6f900c939b284ebcb743b63f324591aef3774a76",
-                "reference": "6f900c939b284ebcb743b63f324591aef3774a76",
+                "url": "https://api.github.com/repos/symfony/ux-autocomplete/zipball/0966e60072be7a5b5cb433675254c7e9e4c93517",
+                "reference": "0966e60072be7a5b5cb433675254c7e9e4c93517",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/dependency-injection": "^6.3|^7.0",
-                "symfony/deprecation-contracts": "^2.5|^3",
                 "symfony/http-foundation": "^6.3|^7.0",
                 "symfony/http-kernel": "^6.3|^7.0",
                 "symfony/property-access": "^6.3|^7.0",
@@ -7516,7 +7515,7 @@
             "require-dev": {
                 "doctrine/collections": "^1.6.8|^2.0",
                 "doctrine/doctrine-bundle": "^2.4.3",
-                "doctrine/orm": "^2.9.4|^3.0",
+                "doctrine/orm": "^2.9.4",
                 "fakerphp/faker": "^1.22",
                 "mtdowling/jmespath.php": "^2.6",
                 "symfony/form": "^6.3|^7.0",
@@ -7562,7 +7561,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-autocomplete/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-autocomplete/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -7578,7 +7577,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-20T16:11:17+00:00"
+            "time": "2024-01-30T15:40:36+00:00"
         },
         {
             "name": "symfony/ux-chartjs",
@@ -7586,12 +7585,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/ux-chartjs.git",
-                "reference": "9f9d9b33627c6f6dcf59f29f4f955172da3c437d"
+                "reference": "d38c57ece51c0a06bd2196f8c24be1c4b009a7ba"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/ux-chartjs/zipball/9f9d9b33627c6f6dcf59f29f4f955172da3c437d",
-                "reference": "9f9d9b33627c6f6dcf59f29f4f955172da3c437d",
+                "url": "https://api.github.com/repos/symfony/ux-chartjs/zipball/d38c57ece51c0a06bd2196f8c24be1c4b009a7ba",
+                "reference": "d38c57ece51c0a06bd2196f8c24be1c4b009a7ba",
                 "shasum": ""
             },
             "require": {
@@ -7643,7 +7642,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-chartjs/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-chartjs/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -7659,7 +7658,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-20T16:11:17+00:00"
+            "time": "2024-01-30T15:40:36+00:00"
         },
         {
             "name": "symfony/ux-cropperjs",
@@ -7729,7 +7728,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-cropperjs/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-cropperjs/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -7809,7 +7808,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-dropzone/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-dropzone/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -7833,12 +7832,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/ux-lazy-image.git",
-                "reference": "3d34714eb60d105e32ddbe12234172c05a99c295"
+                "reference": "5dd07ae887f7cdfb825d649a445b0a76b3ca1883"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/ux-lazy-image/zipball/3d34714eb60d105e32ddbe12234172c05a99c295",
-                "reference": "3d34714eb60d105e32ddbe12234172c05a99c295",
+                "url": "https://api.github.com/repos/symfony/ux-lazy-image/zipball/5dd07ae887f7cdfb825d649a445b0a76b3ca1883",
+                "reference": "5dd07ae887f7cdfb825d649a445b0a76b3ca1883",
                 "shasum": ""
             },
             "require": {
@@ -7888,7 +7887,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-lazy-image/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-lazy-image/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -7904,7 +7903,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-03T21:01:32+00:00"
+            "time": "2024-01-14T07:27:47+00:00"
         },
         {
             "name": "symfony/ux-live-component",
@@ -7912,19 +7911,19 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/ux-live-component.git",
-                "reference": "3af533fd0bf01c5c5a5daa7801afef9103834236"
+                "reference": "ab8710fa0990efb7b9d34b7ec91acac7b6e60d17"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/3af533fd0bf01c5c5a5daa7801afef9103834236",
-                "reference": "3af533fd0bf01c5c5a5daa7801afef9103834236",
+                "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/ab8710fa0990efb7b9d34b7ec91acac7b6e60d17",
+                "reference": "ab8710fa0990efb7b9d34b7ec91acac7b6e60d17",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/property-access": "^5.4.5|^6.0|^7.0",
                 "symfony/ux-twig-component": "^2.8",
-                "twig/twig": "~3.8.0"
+                "twig/twig": "^2.14.7|^3.0.4"
             },
             "conflict": {
                 "symfony/config": "<5.4.0"
@@ -7943,7 +7942,7 @@
                 "symfony/options-resolver": "^5.4|^6.0|^7.0",
                 "symfony/phpunit-bridge": "^6.0|^7.0",
                 "symfony/property-info": "^5.4|^6.0|^7.0",
-                "symfony/security-bundle": "^5.4|^6.0|^7.0",
+                "symfony/security-csrf": "^5.4|^6.0|^7.0",
                 "symfony/serializer": "^5.4|^6.0|^7.0",
                 "symfony/twig-bundle": "^5.4|^6.0|^7.0",
                 "symfony/validator": "^5.4|^6.0|^7.0",
@@ -7981,7 +7980,7 @@
                 "twig"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-live-component/tree/2.x"
+                "source": "https://github.com/symfony/ux-live-component/tree/v2.14.1"
             },
             "funding": [
                 {
@@ -7997,7 +7996,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-21T00:40:57+00:00"
+            "time": "2024-02-03T01:11:47+00:00"
         },
         {
             "name": "symfony/ux-notify",
@@ -8064,7 +8063,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-notify/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-notify/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -8141,7 +8140,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-react/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-react/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -8222,7 +8221,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-svelte/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-svelte/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -8290,7 +8289,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-swup/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-swup/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -8310,7 +8309,7 @@
         },
         {
             "name": "symfony/ux-toggle-password",
-            "version": "v2.15.0",
+            "version": "v2.14.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/ux-toggle-password.git",
@@ -8370,7 +8369,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-toggle-password/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-toggle-password/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -8394,12 +8393,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/ux-translator.git",
-                "reference": "d29bd7b9b3e1c82fd3521001c86404ba83e877ac"
+                "reference": "802ced1bc3292f65f48acd8704d7b29f1cbdd51c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/ux-translator/zipball/d29bd7b9b3e1c82fd3521001c86404ba83e877ac",
-                "reference": "d29bd7b9b3e1c82fd3521001c86404ba83e877ac",
+                "url": "https://api.github.com/repos/symfony/ux-translator/zipball/802ced1bc3292f65f48acd8704d7b29f1cbdd51c",
+                "reference": "802ced1bc3292f65f48acd8704d7b29f1cbdd51c",
                 "shasum": ""
             },
             "require": {
@@ -8447,7 +8446,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-translator/tree/2.x"
+                "source": "https://github.com/symfony/ux-translator/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -8463,7 +8462,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-21T00:36:36+00:00"
+            "time": "2024-01-14T07:27:47+00:00"
         },
         {
             "name": "symfony/ux-turbo",
@@ -8471,12 +8470,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/ux-turbo.git",
-                "reference": "d3590a43fee73304855dfc8022ccb57b0df9f03d"
+                "reference": "7806d3960ff40f76094015644742041376218c76"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/d3590a43fee73304855dfc8022ccb57b0df9f03d",
-                "reference": "d3590a43fee73304855dfc8022ccb57b0df9f03d",
+                "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/7806d3960ff40f76094015644742041376218c76",
+                "reference": "7806d3960ff40f76094015644742041376218c76",
                 "shasum": ""
             },
             "require": {
@@ -8544,7 +8543,7 @@
                 "turbo-stream"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-turbo/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-turbo/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -8560,7 +8559,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-20T16:11:17+00:00"
+            "time": "2023-11-14T10:21:35+00:00"
         },
         {
             "name": "symfony/ux-twig-component",
@@ -8568,12 +8567,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/ux-twig-component.git",
-                "reference": "09c27e13d48dfa4a8d93f55d4b11a6f13d174060"
+                "reference": "b5f593295bd68485cedc59144d285db18ccfb776"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/09c27e13d48dfa4a8d93f55d4b11a6f13d174060",
-                "reference": "09c27e13d48dfa4a8d93f55d4b11a6f13d174060",
+                "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/b5f593295bd68485cedc59144d285db18ccfb776",
+                "reference": "b5f593295bd68485cedc59144d285db18ccfb776",
                 "shasum": ""
             },
             "require": {
@@ -8582,7 +8581,7 @@
                 "symfony/deprecation-contracts": "^2.2|^3.0",
                 "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
                 "symfony/property-access": "^5.4|^6.0|^7.0",
-                "twig/twig": "~3.8.0"
+                "twig/twig": "^2.14.7|^3.0.4"
             },
             "conflict": {
                 "symfony/config": "<5.4.0"
@@ -8629,7 +8628,7 @@
                 "twig"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-twig-component/tree/2.x"
+                "source": "https://github.com/symfony/ux-twig-component/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -8645,7 +8644,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-21T00:38:54+00:00"
+            "time": "2024-01-30T15:40:36+00:00"
         },
         {
             "name": "symfony/ux-typed",
@@ -8697,7 +8696,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-typed/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-typed/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -8778,7 +8777,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-vue/tree/v2.15.0"
+                "source": "https://github.com/symfony/ux-vue/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -9209,16 +9208,16 @@
         },
         {
             "name": "symfonycasts/dynamic-forms",
-            "version": "v0.1.2",
+            "version": "v0.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SymfonyCasts/dynamic-forms.git",
-                "reference": "1bfe8441e3be250b14dc2bd0ae0323dd4acbfd2c"
+                "reference": "dae0100d759f5282edd249232cdf5025c3e0f421"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SymfonyCasts/dynamic-forms/zipball/1bfe8441e3be250b14dc2bd0ae0323dd4acbfd2c",
-                "reference": "1bfe8441e3be250b14dc2bd0ae0323dd4acbfd2c",
+                "url": "https://api.github.com/repos/SymfonyCasts/dynamic-forms/zipball/dae0100d759f5282edd249232cdf5025c3e0f421",
+                "reference": "dae0100d759f5282edd249232cdf5025c3e0f421",
                 "shasum": ""
             },
             "require": {
@@ -9228,9 +9227,8 @@
             "require-dev": {
                 "phpstan/phpstan": "^1.11",
                 "symfony/framework-bundle": "^6.3|^7.0",
-                "symfony/options-resolver": "^5.4|^6.3|^7.0",
                 "symfony/phpunit-bridge": "^5.4|^6.3|^7.0",
-                "symfony/twig-bundle": "^5.4|^6.3|^7.0",
+                "symfony/twig-bundle": "^6.3|^7.0",
                 "twig/twig": "^2.15|^3.0",
                 "zenstruck/browser": "^1.4"
             },
@@ -9257,9 +9255,9 @@
             ],
             "support": {
                 "issues": "https://github.com/SymfonyCasts/dynamic-forms/issues",
-                "source": "https://github.com/SymfonyCasts/dynamic-forms/tree/v0.1.2"
+                "source": "https://github.com/SymfonyCasts/dynamic-forms/tree/v0.1.1"
             },
-            "time": "2024-02-20T11:55:15+00:00"
+            "time": "2023-09-22T11:00:48+00:00"
         },
         {
             "name": "symfonycasts/sass-bundle",
@@ -9318,16 +9316,16 @@
         },
         {
             "name": "twbs/bootstrap",
-            "version": "v5.3.3",
+            "version": "v5.3.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twbs/bootstrap.git",
-                "reference": "6e1f75f420f68e1d52733b8e407fc7c3766c9dba"
+                "reference": "344e912d04b5b6a04482113eff20ab416ff01048"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twbs/bootstrap/zipball/6e1f75f420f68e1d52733b8e407fc7c3766c9dba",
-                "reference": "6e1f75f420f68e1d52733b8e407fc7c3766c9dba",
+                "url": "https://api.github.com/repos/twbs/bootstrap/zipball/344e912d04b5b6a04482113eff20ab416ff01048",
+                "reference": "344e912d04b5b6a04482113eff20ab416ff01048",
                 "shasum": ""
             },
             "replace": {
@@ -9362,9 +9360,9 @@
             ],
             "support": {
                 "issues": "https://github.com/twbs/bootstrap/issues",
-                "source": "https://github.com/twbs/bootstrap/tree/v5.3.3"
+                "source": "https://github.com/twbs/bootstrap/tree/v5.3.2"
             },
-            "time": "2024-02-20T15:14:29+00:00"
+            "time": "2023-09-14T14:19:27+00:00"
         },
         {
             "name": "twig/extra-bundle",
@@ -11796,44 +11794,44 @@
         },
         {
             "name": "symfony/maker-bundle",
-            "version": "v1.55.1",
+            "version": "v1.53.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/maker-bundle.git",
-                "reference": "11a9d3125c5b93ab4043f0f2e9927fdc55881c17"
+                "reference": "8d2f3f96704766837548d177fe3ae39ae94822d9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/11a9d3125c5b93ab4043f0f2e9927fdc55881c17",
-                "reference": "11a9d3125c5b93ab4043f0f2e9927fdc55881c17",
+                "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/8d2f3f96704766837548d177fe3ae39ae94822d9",
+                "reference": "8d2f3f96704766837548d177fe3ae39ae94822d9",
                 "shasum": ""
             },
             "require": {
                 "doctrine/inflector": "^2.0",
                 "nikic/php-parser": "^4.18|^5.0",
                 "php": ">=8.1",
-                "symfony/config": "^6.4|^7.0",
-                "symfony/console": "^6.4|^7.0",
-                "symfony/dependency-injection": "^6.4|^7.0",
+                "symfony/config": "^6.3|^7.0",
+                "symfony/console": "^6.3|^7.0",
+                "symfony/dependency-injection": "^6.3|^7.0",
                 "symfony/deprecation-contracts": "^2.2|^3",
-                "symfony/filesystem": "^6.4|^7.0",
-                "symfony/finder": "^6.4|^7.0",
-                "symfony/framework-bundle": "^6.4|^7.0",
-                "symfony/http-kernel": "^6.4|^7.0",
-                "symfony/process": "^6.4|^7.0"
+                "symfony/filesystem": "^6.3|^7.0",
+                "symfony/finder": "^6.3|^7.0",
+                "symfony/framework-bundle": "^6.3|^7.0",
+                "symfony/http-kernel": "^6.3|^7.0",
+                "symfony/process": "^6.3|^7.0"
             },
             "conflict": {
-                "doctrine/doctrine-bundle": "<2.10",
-                "doctrine/orm": "<2.15"
+                "doctrine/doctrine-bundle": "<2.4",
+                "doctrine/orm": "<2.10"
             },
             "require-dev": {
                 "composer/semver": "^3.0",
                 "doctrine/doctrine-bundle": "^2.5.0",
-                "doctrine/orm": "^2.15|^3",
-                "symfony/http-client": "^6.4|^7.0",
-                "symfony/phpunit-bridge": "^6.4.1|^7.0",
-                "symfony/security-core": "^6.4|^7.0",
-                "symfony/yaml": "^6.4|^7.0",
+                "doctrine/orm": "^2.10.0",
+                "symfony/http-client": "^6.3|^7.0",
+                "symfony/phpunit-bridge": "^6.3|^7.0",
+                "symfony/security-core": "^6.3|^7.0",
+                "symfony/yaml": "^6.3|^7.0",
                 "twig/twig": "^3.0|^4.x-dev"
             },
             "type": "symfony-bundle",
@@ -11868,7 +11866,7 @@
             ],
             "support": {
                 "issues": "https://github.com/symfony/maker-bundle/issues",
-                "source": "https://github.com/symfony/maker-bundle/tree/v1.55.1"
+                "source": "https://github.com/symfony/maker-bundle/tree/v1.53.0"
             },
             "funding": [
                 {
@@ -11884,7 +11882,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-21T13:41:51+00:00"
+            "time": "2024-02-01T10:05:38+00:00"
         },
         {
             "name": "symfony/phpunit-bridge",
@@ -12160,16 +12158,16 @@
         },
         {
             "name": "zenstruck/browser",
-            "version": "v1.8.0",
+            "version": "v1.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/zenstruck/browser.git",
-                "reference": "2c504ce46f76abf6e8725218c0eab519f60b62a6"
+                "reference": "cf514edcf66cd889baeb29870c29a0494d32aff4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/zenstruck/browser/zipball/2c504ce46f76abf6e8725218c0eab519f60b62a6",
-                "reference": "2c504ce46f76abf6e8725218c0eab519f60b62a6",
+                "url": "https://api.github.com/repos/zenstruck/browser/zipball/cf514edcf66cd889baeb29870c29a0494d32aff4",
+                "reference": "cf514edcf66cd889baeb29870c29a0494d32aff4",
                 "shasum": ""
             },
             "require": {
@@ -12217,13 +12215,12 @@
             "description": "A fluent interface for your Symfony functional tests.",
             "homepage": "https://github.com/zenstruck/browser",
             "keywords": [
-                "dev",
                 "symfony",
                 "test"
             ],
             "support": {
                 "issues": "https://github.com/zenstruck/browser/issues",
-                "source": "https://github.com/zenstruck/browser/tree/v1.8.0"
+                "source": "https://github.com/zenstruck/browser/tree/v1.7.0"
             },
             "funding": [
                 {
@@ -12231,7 +12228,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2024-02-07T17:32:28+00:00"
+            "time": "2023-12-14T21:04:01+00:00"
         },
         {
             "name": "zenstruck/callback",
diff --git a/ux.symfony.com/importmap.php b/ux.symfony.com/importmap.php
index 800576c0..6e58beaa 100644
--- a/ux.symfony.com/importmap.php
+++ b/ux.symfony.com/importmap.php
@@ -102,7 +102,7 @@ return [
         'version' => '2.0.2',
     ],
     '@hotwired/turbo' => [
-        'version' => '8.0.2',
+        'version' => '7.3.0',
     ],
     'typed.js' => [
         'version' => '2.0.16',
@@ -141,7 +141,7 @@ return [
         'version' => '11.7.0',
     ],
     'intl-messageformat' => [
-        'version' => '10.5.11',
+        'version' => '10.3.5',
     ],
     '@vue/runtime-dom' => [
         'version' => '3.3.4',
@@ -165,10 +165,10 @@ return [
         'version' => '2.6.2',
     ],
     '@formatjs/icu-messageformat-parser' => [
-        'version' => '2.7.6',
+        'version' => '2.6.2',
     ],
     '@formatjs/icu-skeleton-parser' => [
-        'version' => '1.8.0',
+        'version' => '1.6.2',
     ],
     '@formatjs/fast-memoize' => [
         'version' => '2.2.0',
diff --git a/ux.symfony.com/mercure/.platform.app.yaml b/ux.symfony.com/mercure/.platform.app.yaml
index 3e95d9b8..23ccac7e 100644
--- a/ux.symfony.com/mercure/.platform.app.yaml
+++ b/ux.symfony.com/mercure/.platform.app.yaml
@@ -7,7 +7,7 @@ build:
 
 variables:
     env:
-        MERCURE_TRANSPORT_URL: bolt:///app/.local/mercure.db?size=1000&cleanup_frequency=0.5
+        MERCURE_TRANSPORT_URL: bolt:///app/.local/database.db
         # Defined as platform.sh vars
         # MERCURE_PUBLISHER_JWT_KEY: ''
         # MERCURE_SUBSCRIBER_JWT_KEY: ''
@@ -17,7 +17,7 @@ web:
         socket_family: tcp
         protocol: http
     commands:
-        start: SERVER_NAME=":${PORT}" ./mercure run --config Caddyfile.dev
+        start: SERVER_NAME=":${PORT}" ./mercure run -config Caddyfile.dev
     locations:
         /:
             allow: false
@@ -33,4 +33,4 @@ hooks:
         set -e -x
 
         curl -s https://get.symfony.com/cloud/configurator | (>&2 bash)
-        curl -s -H 'Accept: application/octet-stream' -J -L "https://github.com/dunglas/mercure/releases/download/v0.15.9/mercure_Linux_x86_64.tar.gz" | tar -C /app/ -zxpf -
+        curl -s -H 'Accept: application/octet-stream' -J -L "https://github.com/dunglas/mercure/releases/download/v0.13.0/mercure_0.13.0_Linux_x86_64.tar.gz" | tar -C /app/ -zxpf -
diff --git a/ux.symfony.com/src/LiveMemory/Asset/CssCompiler.php b/ux.symfony.com/src/LiveMemory/Asset/CssCompiler.php
new file mode 100644
index 00000000..fc4b61d7
--- /dev/null
+++ b/ux.symfony.com/src/LiveMemory/Asset/CssCompiler.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\LiveMemory\Asset;
+
+use Symfony\Component\AssetMapper\AssetMapperInterface;
+use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface;
+use Symfony\Component\AssetMapper\MappedAsset;
+use Symfony\Component\Filesystem\Path;
+
+/**
+ * Experimental CSS compiler
+ *
+ * This compiler combines all the SCSS files imported in `live-memory.min.css`
+ * into a single CSS file, and removes SASS comments and empty lines.
+ *
+ * @internal
+ */
+class CssCompiler implements AssetCompilerInterface
+{
+    public function supports(MappedAsset $asset): bool
+    {
+        return 'css' === $asset->publicExtension && str_starts_with($asset->logicalPath, 'styles/demos/live-memory');
+    }
+
+    public function compile(string $content, MappedAsset $asset, AssetMapperInterface $assetMapper): string
+    {
+        return preg_replace_callback('#@import (?:"|\')(.*)(?:\'|");#m', function ($matches) use ($asset) {
+
+            $file = $matches[1];
+            $file = substr_replace($file, '/_', strrpos($file, '/'), 1);
+            $file = './'.$file.'.scss';
+
+            $filePath = Path::makeAbsolute($file, \dirname($asset->sourcePath));
+            if (!file_exists($filePath)) {
+                throw new \RuntimeException(sprintf('The file "%s" does not exist.', $filePath));
+            }
+            $content = file_get_contents($filePath);
+
+            $asset->addFileDependency($filePath);
+
+            // Remove SASS comments
+            $content = preg_replace('#^//.*\n?#m', '', $content);
+            // Remove empty lines
+            $content = preg_replace('/^\s*\n/m', '', $content);
+
+            return $content;
+        }, $content);
+    }
+}
diff --git a/ux.symfony.com/src/Model/RecipeFileTree.php b/ux.symfony.com/src/Model/RecipeFileTree.php
index 7944b449..4a5df23e 100644
--- a/ux.symfony.com/src/Model/RecipeFileTree.php
+++ b/ux.symfony.com/src/Model/RecipeFileTree.php
@@ -23,7 +23,7 @@ class RecipeFileTree
             ->addFile('assets/app.js', 'Your main JavaScript file. It\'s job is to import and load all other files.')
             ->addFile('assets/controllers.json', 'Configures 3rd-party Stimulus controllers. This file is automatically updated when you install a UX package.')
             ->addDirectory('assets/controllers', 'The home of your custom Stimulus controllers!')
-            ->addFile('assets/controllers/hello_controller.js', 'An example controller. Add it to any element with <code class="text-nowrap">data-controller="hello"</code>')
+            ->addFile('assets/controllers/hello_controller.js', 'An example controller. Add it to any element with <code class="text-nowrap">{{ stimulus_controller(\'hello\') }}</code>')
             ->addDirectory('assets/styles')
             ->addFile('assets/styles/app.css', 'Your main CSS file')
             ->addFile('package.json', 'Holds your node dependencies, most importantly Stimulus & Webpack Encore.')
diff --git a/ux.symfony.com/src/Util/SourceCleaner.php b/ux.symfony.com/src/Util/SourceCleaner.php
index b44f95b5..86d98899 100644
--- a/ux.symfony.com/src/Util/SourceCleaner.php
+++ b/ux.symfony.com/src/Util/SourceCleaner.php
@@ -151,13 +151,7 @@ EOF;
     public static function removeExcessHtml(string $content): string
     {
         // remove all HTML attributes and values + whitespace around them
-        $content = preg_replace_callback('/\s+[a-z0-9-]+="[^"]*"/', function ($matches) {
-            if (str_starts_with(trim($matches[0]), 'data-')) {
-                return $matches[0];
-            }
-
-            return '';
-        }, $content);
+        $content = preg_replace('/\s+[a-z0-9-]+="[^"]*"/', '', $content);
 
         // Find all the <div> elements without attributes
         preg_match_all('/<div>\s*(.*?)\s*<\/div>/s', $content, $matches);
diff --git a/ux.symfony.com/templates/base.html.twig b/ux.symfony.com/templates/base.html.twig
index 9b3a562a..edeccc30 100644
--- a/ux.symfony.com/templates/base.html.twig
+++ b/ux.symfony.com/templates/base.html.twig
@@ -10,7 +10,6 @@
         {% if meta.canonical|default %}
             <link rel="canonical" href="{{ meta.canonical }}">
         {% endif %}
-        <meta name="view-transition" content="same-origin" />
         <link rel="icon" href="/favicon.ico" sizes="48x48">
         <link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml">
         <link rel="apple-touch-icon" href="/apple-touch-icon.png">
diff --git a/ux.symfony.com/templates/components/BootstrapModal.html.twig b/ux.symfony.com/templates/components/BootstrapModal.html.twig
index 1158b002..6e4d44aa 100644
--- a/ux.symfony.com/templates/components/BootstrapModal.html.twig
+++ b/ux.symfony.com/templates/components/BootstrapModal.html.twig
@@ -3,9 +3,7 @@
     tabindex: '-1',
     'aria-hidden': 'true',
     id: id ? id : false,
-}) }}
-    data-controller="bootstrap-modal"
->
+}).defaults(stimulus_controller('bootstrap-modal')) }}>
     <div class="modal-dialog">
         <div class="modal-content">
             {% block modal_full_content %}
diff --git a/ux.symfony.com/templates/components/CodeBlock.html.twig b/ux.symfony.com/templates/components/CodeBlock.html.twig
index c95941a4..61e7c85a 100644
--- a/ux.symfony.com/templates/components/CodeBlock.html.twig
+++ b/ux.symfony.com/templates/components/CodeBlock.html.twig
@@ -11,19 +11,19 @@
         </div>
     {% endif %}
 
-    <div class="Terminal_body" data-controller="code-expander">
+    <div class="Terminal_body" {{ stimulus_controller('code-expander') }}>
         {% if not showFilename %}
             <div class="Terminal_actions">
                 <twig:CodeBlockButtons source="{{ this.rawSource }}" link="{{ this.githubLink }}"/>
             </div>
         {% endif %}
         <div class="Terminal_content" style="height: {{ height }};"
-             data-code-expander-target="codeContent"
-             data-controller="code-highlighter"
+            {{ stimulus_target('code-expander', 'codeContent') }}
+            {{ stimulus_controller('code-highlighter') }}
         >
             {% for piece in this.prepareSource %}
                 {% if piece.highlight ?? true %}
-                    <pre><code class="language-{{ this.language }}" data-code-highlighter-target="codeBlock">
+                    <pre><code class="language-{{ this.language }}" {{ stimulus_target('code-highlighter', 'codeBlock') }}>
                         {{- piece.content -}}
                     </code></pre>
                 {% else %}
@@ -36,13 +36,13 @@
             <button
                 type="button"
                 class="btn btn-expand-code pt-2 text-center"
-                data-code-expander-target="expandCodeButton"
-                data-action="code-expander#expandCode"
+                {{ stimulus_target('code-expander', 'expandCodeButton') }}
+                {{ stimulus_action('code-expander', 'expandCode') }}
                 style="display: none;"
             >
-                <twig:Icon name="arrow-down"/>
+                <twig:Icon name="arrow-down" />
                 Expand code
-                <twig:Icon name="arrow-down"/>
+                <twig:Icon name="arrow-down" />
             </button>
         {% endblock %}
     </div>
diff --git a/ux.symfony.com/templates/components/CodeBlockButtons.html.twig b/ux.symfony.com/templates/components/CodeBlockButtons.html.twig
index 0a83aea4..ba185371 100644
--- a/ux.symfony.com/templates/components/CodeBlockButtons.html.twig
+++ b/ux.symfony.com/templates/components/CodeBlockButtons.html.twig
@@ -1,8 +1,8 @@
 {% props source, link %}
 
-<div
-    data-controller="ux-clipboard tooltips"
-    data-ux-clipboard-source-value="{{ source }}"
+<div {{ stimulus_controller('ux-clipboard', {
+    source: source,
+})|stimulus_controller('tooltips') }}
     data-ux-clipboard-success-content="Copied!"
     class="code-buttons"
 >
@@ -19,8 +19,8 @@
     <button
         type="button"
         class="btn btn-copy btn-sm"
-        data-action="ux-clipboard#copy"
+        {{ stimulus_action('ux-clipboard', 'copy') }}
     ><twig:Icon name="copy"/>
-        <span data-ux-clipboard-target="button"> Copy</span>
+        <span {{ stimulus_target('ux-clipboard', 'button') }}> Copy</span>
     </button>
 </div>
diff --git a/ux.symfony.com/templates/components/DinoChart.html.twig b/ux.symfony.com/templates/components/DinoChart.html.twig
index 395f5ef1..5f1da685 100644
--- a/ux.symfony.com/templates/components/DinoChart.html.twig
+++ b/ux.symfony.com/templates/components/DinoChart.html.twig
@@ -34,7 +34,7 @@
                 data-model="currentTypes"
                 multiple
                 id="dinoTypes"
-                data-controller="symfony--ux-autocomplete--autocomplete"
+                {{ stimulus_controller('symfony/ux-autocomplete/autocomplete') }}
             >
                 {% for type in allTypes %}
                     <option value="{{ type }}">{{ type }}</option>
diff --git a/ux.symfony.com/templates/components/HomepageTerminalSwapper.html.twig b/ux.symfony.com/templates/components/HomepageTerminalSwapper.html.twig
index 244e65dc..35ed984f 100644
--- a/ux.symfony.com/templates/components/HomepageTerminalSwapper.html.twig
+++ b/ux.symfony.com/templates/components/HomepageTerminalSwapper.html.twig
@@ -5,13 +5,14 @@
         {% block content -%}
             <span
                 style="font-size: 1.2em;"
-                data-controller="symfony--ux-typed"
-                data-symfony--ux-typed-loop-value="true"
-                data-symfony--ux-typed-show-cursor-value="true"
-                data-symfony--ux-typed-type-speed-value="20"
-                data-symfony--ux-typed-cursor-char-value="🖱"
-                data-symfony--ux-typed-content-type-value="html"
-                data-symfony--ux-typed-strings-value="{{ typedStrings|json_encode|e('html_attr') }}"
+                {{ stimulus_controller('symfony/ux-typed', {
+                    strings: typedStrings,
+                    loop: true,
+                    showCursor: true,
+                    typeSpeed: 20,
+                    cursorChar: '🖱',
+                    contentType: 'html'
+                }) }}
             ></span>
         {%- endblock %}
     {% endcomponent %}
diff --git a/ux.symfony.com/templates/components/TerminalCommand.html.twig b/ux.symfony.com/templates/components/TerminalCommand.html.twig
index 25c305ac..e29eb679 100644
--- a/ux.symfony.com/templates/components/TerminalCommand.html.twig
+++ b/ux.symfony.com/templates/components/TerminalCommand.html.twig
@@ -1,13 +1,13 @@
-<div {{ attributes.defaults({class: 'TerminalCommand'}) }} data-controller="ux-clipboard">
+<div {{ attributes.defaults({class: 'TerminalCommand'}).defaults(stimulus_controller('clipboard')) }}>
     <pre
         class="TerminalCommand_input"
         aria-label="{{ label|default }}"
-    ><code data-clipboard-target="source">{{ command }}</code></pre>
+    ><code {{ stimulus_target('clipboard', 'source') }}>{{ command }}</code></pre>
     <button
         type="button"
         class="TerminalCommand_action btn btn-copy"
         aria-label="Copy command"
-        data-action="clipboard#copy"
+        {{ stimulus_action('clipboard', 'copy') }}
     >
         <twig:Icon name="copy"/>
     </button>
diff --git a/ux.symfony.com/templates/liveDemoBase.html.twig b/ux.symfony.com/templates/liveDemoBase.html.twig
index 6eb9bcfa..2b68c718 100644
--- a/ux.symfony.com/templates/liveDemoBase.html.twig
+++ b/ux.symfony.com/templates/liveDemoBase.html.twig
@@ -68,6 +68,7 @@
 
 {% block aside %}
     <aside style="background-color: var(--bs-secondary-bg-subtle);" class="mt-5">
+
         <twig:PrevNextDemo demo="{{ demo }}" />
-    </aside>
+    </aside>>
 {% endblock %}
diff --git a/ux.symfony.com/templates/main/homepage.html.twig b/ux.symfony.com/templates/main/homepage.html.twig
index e6f32ee3..03f6a7d3 100644
--- a/ux.symfony.com/templates/main/homepage.html.twig
+++ b/ux.symfony.com/templates/main/homepage.html.twig
@@ -37,7 +37,7 @@
         </div>
 
         <div class="col-12 col-md-6 mt-3 mt-md-0">
-            <div data-controller="tooltips" class="ux-container file-tree flex-grow-1 ms-2 py-3">
+            <div {{ stimulus_controller('tooltips') }} class="ux-container file-tree flex-grow-1 ms-2 py-3">
                 <div style="height: 185px; overflow: auto;">
                     <ul class="list-unstyled file-tree-main">
                         {{ include('main/_fileTreeLevel.html.twig', {
@@ -77,19 +77,19 @@
     </div>
 
     {% block markdown_example %}
-        <div class="p-4 markdown-container background-glass shadow-blur shadow-blur--rainbow  mt-5 row" data-controller="markdown">
+        <div class="p-4 markdown-container background-glass shadow-blur shadow-blur--rainbow  mt-5 row" {{ stimulus_controller('markdown') }}>
             <div class="col-12 col-md-5">
                 <textarea rows="3" class="form-control" aria-label="Type markdown into this box"
-                          data-markdown-target="input"
+                    {{ stimulus_target('markdown', 'input') }}
                 >Writing JavaScript is a **dream** with Stimulus 🤩</textarea>
             </div>
             <div class="col-12 col-md-2 d-flex justify-content-center align-items-center py-3">
-                <button class="btn btn-dark" data-action="markdown#render">
-                    Convert <twig:Icon name="arrow-right"/>
-                </button>
+                <button class="btn btn-dark"
+                    {{ stimulus_action('markdown', 'render') }}
+                >Convert <twig:Icon name="arrow-right"/></button>
             </div>
             <div class="col-12 col-md-5">
-                <div style="min-height: 86px;" class="markdown-form-render-container p-2" data-markdown-target="preview">
+                <div style="min-height: 86px;" class="markdown-form-render-container p-2" {{ stimulus_target('markdown', 'preview') }} >
                     <small class="fw-light">(click "Convert")</small>
                 </div>
             </div>
diff --git a/ux.symfony.com/templates/ux_packages/lazy-image.html.twig b/ux.symfony.com/templates/ux_packages/lazy-image.html.twig
index 021d5536..03c73ce2 100644
--- a/ux.symfony.com/templates/ux_packages/lazy-image.html.twig
+++ b/ux.symfony.com/templates/ux_packages/lazy-image.html.twig
@@ -41,8 +41,9 @@
 {% block code_block_right %}
     <img
         src="{{ data_uri_thumbnail(legosFilePath, 40, 30) }}"
-        data-controller="symfony--ux-lazy-image--lazy-image"
-        data-symfony--ux-lazy-image--lazy-image-src="{{ asset('images/legos.jpg') }}"
+        {{ stimulus_controller('symfony/ux-lazy-image/lazy-image', {
+            src: asset('images/legos.jpg')
+        }) }}
         alt="A blurhash image that is replaced by the real image on load"
         width="578"
         height="275"
diff --git a/ux.symfony.com/templates/ux_packages/swup.html.twig b/ux.symfony.com/templates/ux_packages/swup.html.twig
index eba9c937..6aa4c44a 100644
--- a/ux.symfony.com/templates/ux_packages/swup.html.twig
+++ b/ux.symfony.com/templates/ux_packages/swup.html.twig
@@ -30,7 +30,7 @@
 
 {% block demo_content %}
     {% block swup_specific_demo_content %}
-        <div data-controller="symfony--ux-swup--swup" id="swup" data-turbo="false">
+        <div {{ stimulus_controller('symfony/ux-swup/swup') }} id="swup" data-turbo="false">
             <div class="row">
                 {% for package in pager %}
                     {{ include('main/_package_in_list.html.twig') }}
diff --git a/ux.symfony.com/templates/ux_packages/translator.html.twig b/ux.symfony.com/templates/ux_packages/translator.html.twig
index cb9ecef9..94fdbcdc 100644
--- a/ux.symfony.com/templates/ux_packages/translator.html.twig
+++ b/ux.symfony.com/templates/ux_packages/translator.html.twig
@@ -26,12 +26,12 @@
 {% block demo_title %}UX Translator{% endblock %}
 
 {% block demo_content %}
-    <div data-controller="translator-demo">
-        <div data-translator-demo-target="localeSwitcher" style="position: absolute; top: 16px; right: 16px ">
+    <div {{ stimulus_controller('translator-demo') }}>
+        <div {{ stimulus_target('translator-demo', 'localeSwitcher') }} style="position: absolute; top: 16px; right: 16px ">
             <div class="row">
                 <label for="translator-demo-locale" class="col-3 col-form-label">Locale</label>
                 <div class="col">
-                    <select id="translator-demo-locale" class="form-select" data-action="translator-demo#setLocale">
+                    <select id="translator-demo-locale" {{ stimulus_action('translator-demo', 'setLocale') }} class="form-select">
                         <option value="en">🇬🇧 English</option>
                         <option value="fr">🇫🇷 French</option>
                     </select>
@@ -54,22 +54,22 @@
         <div class="row mb-4">
             <div class="col-5">
                 <div class="Terminal_body">
-                    <pre data-translator-demo-target="helloCode"></pre>
+                    <pre {{ stimulus_target('translator-demo', 'helloCode') }}></pre>
                 </div>
             </div>
             <div class="col">
-                <span data-translator-demo-target="helloOutput"></span>
+                <span {{ stimulus_target('translator-demo', 'helloOutput') }}></span>
             </div>
         </div>
 
         <div class="row mb-4">
             <div class="col-5">
                 <div class="Terminal_body">
-                    <pre data-translator-demo-target="sayHelloCode"></pre>
+                    <pre {{ stimulus_target('translator-demo', 'sayHelloCode') }}></pre>
                 </div>
             </div>
             <div class="col">
-                <span data-translator-demo-target="sayHelloOutput"></span>
+                <span {{ stimulus_target('translator-demo', 'sayHelloOutput') }}></span>
             </div>
             <div class="col-4">
                 <div class="row mb-3">
@@ -80,8 +80,8 @@
                             value="Fabien"
                             class="form-control"
                             id="sayHelloNameInput"
-                            data-translator-demo-target="sayHelloNameInput"
-                            data-action="translator-demo#render"
+                            {{ stimulus_target('translator-demo', 'sayHelloNameInput') }}
+                            {{ stimulus_action('translator-demo', 'render') }}
                         >
                     </div>
                 </div>
@@ -91,11 +91,11 @@
         <div class="row mb-4">
             <div class="col-5">
                 <div class="Terminal_body">
-                    <pre data-translator-demo-target="invitationTitleCode"></pre>
+                    <pre {{ stimulus_target('translator-demo', 'invitationTitleCode') }}></pre>
                 </div>
             </div>
             <div class="col">
-                <span data-translator-demo-target="invitationTitleOutput"></span>
+                <span {{ stimulus_target('translator-demo', 'invitationTitleOutput') }}></span>
             </div>
             <div class="col-4">
                 <div class="row mb-3">
@@ -104,8 +104,8 @@
                         <select
                             id="invitationTitleOrganizationGenderInput"
                             class="form-select"
-                            data-translator-demo-target="invitationTitleOrganizationGenderInput"
-                            data-action="translator-demo#render"
+                            {{ stimulus_target('translator-demo', 'invitationTitleOrganizationGenderInput') }}
+                            {{ stimulus_action('translator-demo', 'render') }}
                         >
                             <option value="female">Female</option>
                             <option value="male">Male</option>
@@ -123,8 +123,8 @@
                             type="text"
                             value="Clara"
                             class="form-control"
-                            data-translator-demo-target="invitationTitleOrganizationNameInput"
-                            data-action="translator-demo#render"
+                            {{ stimulus_target('translator-demo', 'invitationTitleOrganizationNameInput') }}
+                            {{ stimulus_action('translator-demo', 'render') }}
                         >
                     </div>
                 </div>
@@ -134,11 +134,11 @@
         <div class="row mb-4">
             <div class="col-5">
                 <div class="Terminal_body">
-                    <pre data-translator-demo-target="numOfApplesCode"></pre>
+                    <pre {{ stimulus_target('translator-demo', 'numOfApplesCode') }}></pre>
                 </div>
             </div>
             <div class="col">
-                <span data-translator-demo-target="numOfApplesOutput"></span>
+                <span {{ stimulus_target('translator-demo', 'numOfApplesOutput') }}></span>
             </div>
             <div class="col-4">
                 <div class="row mb-3">
@@ -151,8 +151,8 @@
                             value="2"
                             class="form-control"
                             id="numOfApplesCountInput"
-                            data-translator-demo-target="numOfApplesCountInput"
-                            data-action="translator-demo#render"
+                            {{ stimulus_target('translator-demo', 'numOfApplesCountInput') }}
+                            {{ stimulus_action('translator-demo', 'render') }}
                         >
                     </div>
                 </div>
@@ -162,11 +162,11 @@
         <div class="row mb-4">
             <div class="col-5">
                 <div class="Terminal_body">
-                    <pre data-translator-demo-target="finishPlaceCode"></pre>
+                    <pre {{ stimulus_target('translator-demo', 'finishPlaceCode') }}></pre>
                 </div>
             </div>
             <div class="col">
-                <span data-translator-demo-target="finishPlaceOutput"></span>
+                <span {{ stimulus_target('translator-demo', 'finishPlaceOutput') }}></span>
             </div>
             <div class="col-4">
                 <div class="row mb-3">
@@ -178,8 +178,8 @@
                             value="1"
                             class="form-control"
                             id="finishPlacePlaceInput"
-                            data-translator-demo-target="finishPlacePlaceInput"
-                            data-action="translator-demo#render"
+                            {{ stimulus_target('translator-demo', 'finishPlacePlaceInput') }}
+                            {{ stimulus_action('translator-demo', 'render') }}
                         >
                     </div>
                 </div>
@@ -189,11 +189,11 @@
         <div class="row mb-4">
             <div class="col-5">
                 <div class="Terminal_body">
-                    <pre data-translator-demo-target="publishedAtCode"></pre>
+                    <pre {{ stimulus_target('translator-demo', 'publishedAtCode') }}></pre>
                 </div>
             </div>
             <div class="col">
-                <span data-translator-demo-target="publishedAtOutput"></span>
+                <span {{ stimulus_target('translator-demo', 'publishedAtOutput') }}></span>
             </div>
             <div class="col-4">
                 <div class="row mb-3">
@@ -204,8 +204,8 @@
                             value="2023-04-27 08:12"
                             class="form-control"
                             id="publishedAtDateInput"
-                            data-translator-demo-target="publishedAtDateInput"
-                            data-action="translator-demo#render"
+                            {{ stimulus_target('translator-demo', 'publishedAtDateInput') }}
+                            {{ stimulus_action('translator-demo', 'render') }}
                         >
                     </div>
                 </div>
@@ -215,11 +215,11 @@
         <div class="row mb-4">
             <div class="col-5">
                 <div class="Terminal_body">
-                    <pre data-translator-demo-target="progressAtCode"></pre>
+                    <pre {{ stimulus_target('translator-demo', 'progressAtCode') }}></pre>
                 </div>
             </div>
             <div class="col">
-                <span data-translator-demo-target="progressAtOutput"></span>
+                <span {{ stimulus_target('translator-demo', 'progressAtOutput') }}></span>
             </div>
             <div class="col-4">
                 <div class="row mb-3">
@@ -229,8 +229,8 @@
                             id="progressAtProgressInput"
                             type="range"
                             class="form-range"
-                            data-translator-demo-target="progressAtProgressInput"
-                            data-action="translator-demo#render"
+                            {{ stimulus_target('translator-demo', 'progressAtProgressInput') }}
+                            {{ stimulus_action('translator-demo', 'render') }}
                         />
                     </div>
                 </div>
@@ -240,11 +240,11 @@
         <div class="row">
             <div class="col-5">
                 <div class="Terminal_body">
-                <pre data-translator-demo-target="valueOfObjectCode"></pre>
+                <pre {{ stimulus_target('translator-demo', 'valueOfObjectCode') }}></pre>
                 </div>
             </div>
             <div class="col">
-                <span data-translator-demo-target="valueOfObjectOutput"></span>
+                <span {{ stimulus_target('translator-demo', 'valueOfObjectOutput') }}></span>
             </div>
             <div class="col-4">
                 <div class="row mb-3">
@@ -256,8 +256,8 @@
                             value="30"
                             class="form-control"
                             id="valueOfObjectValueInput"
-                            data-translator-demo-target="valueOfObjectValueInput"
-                            data-action="translator-demo#render"
+                            {{ stimulus_target('translator-demo', 'valueOfObjectValueInput') }}
+                            {{ stimulus_action('translator-demo', 'render') }}
                         >
                 </div>
             </div>
diff --git a/ux.symfony.com/templates/ux_packages/typed.html.twig b/ux.symfony.com/templates/ux_packages/typed.html.twig
index 495989b5..0fba5b88 100644
--- a/ux.symfony.com/templates/ux_packages/typed.html.twig
+++ b/ux.symfony.com/templates/ux_packages/typed.html.twig
@@ -25,21 +25,21 @@
 
 {% block code_block_right %}
     <div class="p-5" style="font-size: 1.3em;">
-        {% set strings = [
-            'I ❤️ Symfony UX!',
-            'Symfony UX Typed loves to type',
-            'Symfony UX Typed and backspace',
-            'Control the speed',
-            'Control the cursor',
-            'Control your destiny!!!',
-            'Control your destiny... sort of',
-        ] %}
         <span
-            data-controller="symfony--ux-typed"
-            data-symfony--ux-typed-loop-value="true"
-            data-symfony--ux-typed-show-cursor-value="true"
-            data-symfony--ux-typed-cursor-char-value="✨"
-            data-symfony--ux-typed-strings-value="{{ strings|json_encode|e('html_attr') }}"
+            {{ stimulus_controller('symfony/ux-typed', {
+                strings: [
+                    'I ❤️ Symfony UX!',
+                    'Symfony UX Typed loves to type',
+                    'Symfony UX Typed and backspace',
+                    'Control the speed',
+                    'Control the cursor',
+                    'Control your destiny!!!',
+                    'Control your destiny... sort of',
+                ],
+                loop: true,
+                showCursor: true,
+                cursorChar: '✨',
+            }) }}
         ></span>
     </div>
 {% endblock %}


Test patch:
diff --git a/src/Autocomplete/assets/test/controller.test.ts b/src/Autocomplete/assets/test/controller.test.ts
index f7c5060f..440e2004 100644
--- a/src/Autocomplete/assets/test/controller.test.ts
+++ b/src/Autocomplete/assets/test/controller.test.ts
@@ -815,46 +815,4 @@ describe('AutocompleteController', () => {
         });
         expect(getSelectedValues()).toEqual(['2', '3']);
     });
-
-    it('does not trigger a reset when the style of "multiple" attribute changes', async () => {
-        const { container } = await startAutocompleteTest(`
-            <select multiple data-testid='main-element' data-controller='autocomplete'>
-                <option value=''>Select dogs</option>
-                <option value='1'>dog1</option>
-                <option value='2'>dog2</option>
-                <option value='3'>dog3</option>
-            </select>
-        `);
-
-        let wasReset = false;
-        container.addEventListener('autocomplete:before-reset', () => {
-            wasReset = true;
-        });
-
-        const selectElement = getByTestId(container, 'main-element') as HTMLSelectElement;
-        selectElement.setAttribute('multiple', 'multiple');
-        // wait for the mutation observe
-        await shortDelay(10);
-        expect(wasReset).toBe(false);
-    });
-
-    it('does not trigger a reset based on the extra, empty select', async () => {
-        const { container, tomSelect } = await startAutocompleteTest(`
-            <select data-testid='main-element' data-controller='autocomplete'>
-                <option value='1'>dog1</option>
-                <option value='2'>dog2</option>
-                <option value='3'>dog3</option>
-            </select>
-        `);
-
-        let wasReset = false;
-        container.addEventListener('autocomplete:before-reset', () => {
-            wasReset = true;
-        });
-
-        tomSelect.addItem('2');
-        // wait for the mutation observe
-        await shortDelay(10);
-        expect(wasReset).toBe(false);
-    });
 });
diff --git a/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php b/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php
index 0023abe7..1dd30667 100644
--- a/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php
+++ b/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php
@@ -54,7 +54,7 @@ class EntityMetadataTest extends KernelTestCase
         $this->assertEquals(ClassMetadataInfo::MANY_TO_ONE, $metadata->getPropertyDataType('category'));
     }
 
-    public function testGetFieldMetadata(): void
+    public function testGetPropertyMetadata(): void
     {
         $metadata = $this->getMetadata();
         $this->assertSame([
@@ -66,48 +66,7 @@ class EntityMetadataTest extends KernelTestCase
             'nullable' => false,
             'precision' => null,
             'columnName' => 'name',
-        ], $metadata->getFieldMetadata('name'));
-    }
-
-    public function testGetAssociationMetadata(): void
-    {
-        $metadata = $this->getMetadata();
-        $this->assertSame([
-            'fieldName' => 'category',
-            'joinColumns' => [
-                [
-                    'name' => 'category_id',
-                    'unique' => false,
-                    'nullable' => false,
-                    'onDelete' => null,
-                    'columnDefinition' => null,
-                    'referencedColumnName' => 'id',
-                ],
-            ],
-            'cascade' => [],
-            'inversedBy' => 'products',
-            'targetEntity' => 'Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category',
-            'fetch' => 2,
-            'type' => 2,
-            'mappedBy' => null,
-            'isOwningSide' => true,
-            'sourceEntity' => 'Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product',
-            'isCascadeRemove' => false,
-            'isCascadePersist' => false,
-            'isCascadeRefresh' => false,
-            'isCascadeMerge' => false,
-            'isCascadeDetach' => false,
-            'sourceToTargetKeyColumns' => [
-                'category_id' => 'id',
-            ],
-            'joinColumnFieldNames' => [
-                'category_id' => 'category_id',
-            ],
-            'targetToSourceKeyColumns' => [
-                'id' => 'category_id',
-            ],
-            'orphanRemoval' => false,
-        ], $metadata->getAssociationMetadata('category'));
+        ], $metadata->getPropertyMetadata('name'));
     }
 
     public function testIsEmbeddedClassProperty(): void
diff --git a/src/LiveComponent/assets/test/Directive/directives_parser.test.ts b/src/LiveComponent/assets/test/Directive/directives_parser.test.ts
index 8c13c25d..ececf9a9 100644
--- a/src/LiveComponent/assets/test/Directive/directives_parser.test.ts
+++ b/src/LiveComponent/assets/test/Directive/directives_parser.test.ts
@@ -29,6 +29,7 @@ describe('directives parser', () => {
         assertDirectiveEquals(directives[0], {
             action: 'hide',
             args: [],
+            named: {},
             modifiers: [],
         })
     });
@@ -39,6 +40,7 @@ describe('directives parser', () => {
         assertDirectiveEquals(directives[0], {
             action: 'addClass',
             args: ['opacity-50'],
+            named: {},
             modifiers: [],
         })
     });
@@ -49,6 +51,7 @@ describe('directives parser', () => {
         assertDirectiveEquals(directives[0], {
             action: 'addClass',
             args: ['opacity-50 disabled'],
+            named: {},
             modifiers: [],
         })
     });
@@ -60,6 +63,7 @@ describe('directives parser', () => {
             action: 'addClass',
             // space between arguments is trimmed
             args: ['opacity-50', 'disabled'],
+            named: {},
             modifiers: [],
         })
     });
@@ -70,11 +74,13 @@ describe('directives parser', () => {
         assertDirectiveEquals(directives[0], {
             action: 'addClass',
             args: ['opacity-50'],
+            named: {},
             modifiers: [],
         })
         assertDirectiveEquals(directives[1], {
             action: 'addAttribute',
             args: ['disabled'],
+            named: {},
             modifiers: [],
         })
     });
@@ -85,16 +91,63 @@ describe('directives parser', () => {
         assertDirectiveEquals(directives[0], {
             action: 'hide',
             args: [],
+            named: {},
             modifiers: [],
         })
         assertDirectiveEquals(directives[1], {
             action: 'addClass',
             args: ['opacity-50 disabled'],
+            named: {},
             modifiers: [],
         })
         assertDirectiveEquals(directives[2], {
             action: 'addAttribute',
             args: ['disabled'],
+            named: {},
+            modifiers: [],
+        })
+    });
+
+    it('parses single named argument', () => {
+        const directives = parseDirectives('save(foo=bar)');
+        expect(directives).toHaveLength(1);
+        assertDirectiveEquals(directives[0], {
+            action: 'save',
+            args: [],
+            named: { foo: 'bar' },
+            modifiers: [],
+        })
+    });
+
+    it('parses multiple named arguments', () => {
+        const directives = parseDirectives('save(foo=bar, baz=bazzles)');
+        expect(directives).toHaveLength(1);
+        assertDirectiveEquals(directives[0], {
+            action: 'save',
+            args: [],
+            named: { foo: 'bar', baz: 'bazzles' },
+            modifiers: [],
+        })
+    });
+
+    it('parses arguments and spaces are kept', () => {
+        const directives = parseDirectives('save(foo= bar)');
+        expect(directives).toHaveLength(1);
+        assertDirectiveEquals(directives[0], {
+            action: 'save',
+            args: [],
+            named: { foo: ' bar' },
+            modifiers: [],
+        })
+    });
+
+    it('parses argument names with space is trimmed', () => {
+        const directives = parseDirectives('save(foo  =bar)');
+        expect(directives).toHaveLength(1);
+        assertDirectiveEquals(directives[0], {
+            action: 'save',
+            args: [],
+            named: { foo: 'bar' },
             modifiers: [],
         })
     });
@@ -105,6 +158,7 @@ describe('directives parser', () => {
         assertDirectiveEquals(directives[0], {
             action: 'addClass',
             args: ['disabled'],
+            named: {},
             modifiers: [
                 { name: 'delay', value: null }
             ],
@@ -117,6 +171,7 @@ describe('directives parser', () => {
         assertDirectiveEquals(directives[0], {
             action: 'addClass',
             args: ['disabled'],
+            named: {},
             modifiers: [
                 { name: 'delay', value: '400' },
             ],
@@ -124,11 +179,12 @@ describe('directives parser', () => {
     });
 
     it('parses multiple modifiers', () => {
-        const directives = parseDirectives('prevent|debounce(400)|save');
+        const directives = parseDirectives('prevent|debounce(400)|save(foo=bar)');
         expect(directives).toHaveLength(1);
         assertDirectiveEquals(directives[0], {
             action: 'save',
             args: [],
+            named: { foo: 'bar' },
             modifiers: [
                 { name: 'prevent', value: null },
                 { name: 'debounce', value: '400' },
@@ -155,10 +211,22 @@ describe('directives parser', () => {
             }).toThrow('Missing space after addClass()')
         });
 
+        it('named and unnamed arguments cannot be mixed', () => {
+            expect(() => {
+                parseDirectives('save(foo=bar, baz)');
+            }).toThrow('Normal and named arguments cannot be mixed inside "save()"')
+        });
+
         it('modifier cannot have multiple arguments', () => {
             expect(() => {
                 parseDirectives('debounce(10, 20)|save');
             }).toThrow('The modifier "debounce()" does not support multiple arguments.')
         });
+
+        it('modifier cannot have named arguments', () => {
+            expect(() => {
+                parseDirectives('debounce(foo=bar)|save');
+            }).toThrow('The modifier "debounce()" does not support named arguments.')
+        });
     });
 });
diff --git a/src/LiveComponent/assets/test/controller/action.test.ts b/src/LiveComponent/assets/test/controller/action.test.ts
index d4f7ba9e..cd71646a 100644
--- a/src/LiveComponent/assets/test/controller/action.test.ts
+++ b/src/LiveComponent/assets/test/controller/action.test.ts
@@ -23,7 +23,7 @@ describe('LiveController Action Tests', () => {
             <div ${initComponent(data)}>
                 ${data.isSaved ? 'Comment Saved!' : ''}
 
-                <button data-action="live#action" data-live-action-param="save">Save</button>
+                <button data-action="live#action" data-action-name="save">Save</button>
             </div>
         `);
 
@@ -46,7 +46,7 @@ describe('LiveController Action Tests', () => {
 
                 ${data.isSaved ? 'Comment Saved!' : ''}
 
-                <button data-action="live#action" data-live-action-param="save">Save</button>
+                <button data-action="live#action" data-action-name="save">Save</button>
             </div>
         `);
 
@@ -73,22 +73,16 @@ describe('LiveController Action Tests', () => {
 
     it('Sends action with named args', async () => {
         const test = await createTest({ isSaved: false}, (data: any) => `
-            <div ${initComponent(data)}>
-                ${data.isSaved ? 'Component Saved!' : ''}
+           <div ${initComponent(data)}>
+               ${data.isSaved ? 'Component Saved!' : ''}
 
-                <button
-                    data-action="live#action"
-                    data-live-action-param="sendNamedArgs"
-                    data-live-a-param="1"
-                    data-live-b-param="2"
-                    data-live-c-param="banana"
-                >Send named args</button>
-            </div>
+               <button data-action="live#action" data-action-name="sendNamedArgs(a=1, b=2, c=3)">Send named args</button>
+           </div>
        `);
 
         // ONLY a post is sent, not a re-render GET
         test.expectsAjaxCall()
-            .expectActionCalled('sendNamedArgs', {a: 1, b: 2, c: 'banana'})
+            .expectActionCalled('sendNamedArgs', {a: '1', b: '2', c: '3'})
             .serverWillChangeProps((data: any) => {
                 // server marks component as "saved"
                 data.isSaved = true;
@@ -105,7 +99,7 @@ describe('LiveController Action Tests', () => {
                <select
                    data-model="on(change)|food"
                    data-action="live#action"
-                   data-live-action-param="changeFood"
+                   data-action-name="changeFood"
                >
                    <option value="" ${data.food === '' ? 'selected' : ''}>Choose a food</option>
                    <option value="pizza" ${data.pizza === '' ? 'selected' : ''}>Pizza</option>
@@ -137,7 +131,7 @@ describe('LiveController Action Tests', () => {
 
                 <span>${data.comment}</span>
 
-                <button data-action="live#action" data-live-action-param="save">Save</button>
+                <button data-action="live#action" data-action-name="save">Save</button>
             </div>
         `);
 
@@ -176,8 +170,8 @@ describe('LiveController Action Tests', () => {
         const test = await createTest({ isSaved: false }, (data: any) => `
             <div ${initComponent(data)}>
                 ${data.isSaved ? 'Component Saved!' : ''}
-                <button data-action="live#action" data-live-action-param="debounce(10)|save">Save</button>
-                <button data-action="live#action" data-live-action-param="debounce(10)|sync" data-live-sync-all-param="1">Sync</button>
+                <button data-action="live#action" data-action-name="debounce(10)|save">Save</button>
+                <button data-action="live#action" data-action-name="debounce(10)|sync(syncAll=1)">Sync</button>
             </div>
         `);
 
@@ -185,7 +179,7 @@ describe('LiveController Action Tests', () => {
         test.expectsAjaxCall()
             // 3 actions called
             .expectActionCalled('save')
-            .expectActionCalled('sync', { syncAll: 1 })
+            .expectActionCalled('sync', { syncAll: '1' })
             .expectActionCalled('save')
             .serverWillChangeProps((data: any) => {
                 data.isSaved = true;
diff --git a/src/LiveComponent/assets/test/controller/emit.test.ts b/src/LiveComponent/assets/test/controller/emit.test.ts
index 69f0adb3..15f739ad 100644
--- a/src/LiveComponent/assets/test/controller/emit.test.ts
+++ b/src/LiveComponent/assets/test/controller/emit.test.ts
@@ -26,17 +26,17 @@ describe('LiveController Emit Tests', () => {
                 Render Count: ${data.renderCount}
                 <button
                     data-action="live#emit"
-                    data-live-event-param="fooEvent"
+                    data-event="fooEvent"
                 >Emit Simple</button>
 
                 <button
                     data-action="live#emit"
-                    data-live-event-param="name(simple-component)|fooEvent"
+                    data-event="name(simple-component)|fooEvent"
                 >Emit Named Matching</button>
 
                 <button
                     data-action="live#emit"
-                    data-live-event-param="name(other-component)|fooEvent"
+                    data-event="name(other-component)|fooEvent"
                 >Emit Named Not Matching</button>
             </div>
         `);
diff --git a/src/LiveComponent/assets/test/controller/error.test.ts b/src/LiveComponent/assets/test/controller/error.test.ts
index 5096d5b9..7d79823b 100644
--- a/src/LiveComponent/assets/test/controller/error.test.ts
+++ b/src/LiveComponent/assets/test/controller/error.test.ts
@@ -26,7 +26,7 @@ describe('LiveController Error Handling', () => {
         const test = await createTest({ counter: 4 }, (data: any) => `
             <div ${initComponent(data)}>
                 Current count: ${data.counter}
-                <button data-action="live#action" data-live-action-param="save">Save</button>
+                <button data-action="live#action" data-action-name="save">Save</button>
                 <button data-action="live#$render">Render</button>
             </div>
         `);
@@ -62,7 +62,7 @@ describe('LiveController Error Handling', () => {
         const test = await createTest({ }, (data: any) => `
             <div ${initComponent(data)}>
                 Original component text
-                <button data-action="live#action" data-live-action-param="save">Save</button>
+                <button data-action="live#action" data-action-name="save">Save</button>
             </div>
         `);
 
diff --git a/src/LiveComponent/assets/test/controller/loading.test.ts b/src/LiveComponent/assets/test/controller/loading.test.ts
index 6326661f..ee33967e 100644
--- a/src/LiveComponent/assets/test/controller/loading.test.ts
+++ b/src/LiveComponent/assets/test/controller/loading.test.ts
@@ -81,8 +81,8 @@ describe('LiveController data-loading Tests', () => {
             <div ${initComponent(data)}> 
                 <span data-loading="action(save)|show" data-testid="loading-element">Loading...</span>
 
-                <button data-action="live#action" data-live-action-param="save">Save</button>
-                <button data-action="live#action" data-live-action-param="otherAction">Other Action</button>
+                <button data-action="live#action" data-action-name="save">Save</button>
+                <button data-action="live#action" data-action-name="otherAction">Other Action</button>
                 <button data-action="live#$render">Re-Render</button>
             </div>
         `);
@@ -176,8 +176,8 @@ describe('LiveController data-loading Tests', () => {
             <div ${initComponent(data)}> 
                 <span data-loading="action(otherAction)|show" data-testid="loading-element">Loading...</span>
 
-                <button data-action="live#action" data-live-action-param="debounce(50)|save">Save</button>
-                <button data-action="live#action" data-live-action-param="otherAction">Other Action</button>
+                <button data-action="live#action" data-action-name="debounce(50)|save">Save</button>
+                <button data-action="live#action" data-action-name="otherAction">Other Action</button>
             </div>
         `);
 
@@ -203,7 +203,7 @@ describe('LiveController data-loading Tests', () => {
            <div ${initComponent(data)}> 
                <span data-loading="action(save)|delay(50)|show" data-testid="loading-element">Loading...</span>
 
-               <button data-action="live#action" data-live-action-param="save">Save</button>
+               <button data-action="live#action" data-action-name="save">Save</button>
            </div>
        `);
 
diff --git a/src/LiveComponent/assets/test/controller/model.test.ts b/src/LiveComponent/assets/test/controller/model.test.ts
index 49b30365..81ef9189 100644
--- a/src/LiveComponent/assets/test/controller/model.test.ts
+++ b/src/LiveComponent/assets/test/controller/model.test.ts
@@ -810,9 +810,9 @@ describe('LiveController data-model Tests', () => {
             <div ${initComponent(data)}>
                 <form data-model>
                     <textarea name="comment" data-testid="comment">${data.comment}</textarea>
-                </form>
+               </form>
 
-                <input data-testid="other-input">
+               <button data-action="live#$render">Reload</button>
            </div>
        `);
 
@@ -823,18 +823,17 @@ describe('LiveController data-model Tests', () => {
             // delay slightly so we can type in the textarea
             .delayResponse(10);
 
-        const renderPromise = test.component.render();
+        getByText(test.element, 'Reload').click();
         // mimic changing the field, but without (yet) triggering the change event
         const commentField = getByTestId(test.element, 'comment');
-        const inputField = getByTestId(test.element, 'other-input');
+        if (!(commentField instanceof HTMLTextAreaElement)) {
+            throw new Error('wrong type');
+        }
         userEvent.type(commentField, ' ftw!');
-        // To make the test more robust, make the textarea NOT the active element.
-        // The active element's value is ignored during morphing. Here, we want
-        // to test that even if the morph system *does* want to update the value
-        // of the textarea, the value will be kept.
-        userEvent.type(inputField, 'making this element active');
 
-        await renderPromise;
+        // wait for loading start and end
+        await waitFor(() => expect(test.element).toHaveAttribute('busy'));
+        await waitFor(() => expect(test.element).not.toHaveAttribute('busy'));
 
         expect(commentField).toHaveValue('Live components ftw!');
 
@@ -846,7 +845,10 @@ describe('LiveController data-model Tests', () => {
                 data.comment = 'server changed comment';
             });
 
-        await test.component.render();
+        getByText(test.element, 'Reload').click();
+        // wait for loading start and end
+        await waitFor(() => expect(test.element).toHaveAttribute('busy'));
+        await waitFor(() => expect(test.element).not.toHaveAttribute('busy'));
 
         expect(commentField).toHaveValue('server changed comment');
     });
diff --git a/src/LiveComponent/assets/test/controller/query-binding.test.ts b/src/LiveComponent/assets/test/controller/query-binding.test.ts
index f62754fa..a028bed9 100644
--- a/src/LiveComponent/assets/test/controller/query-binding.test.ts
+++ b/src/LiveComponent/assets/test/controller/query-binding.test.ts
@@ -149,7 +149,7 @@ describe('LiveController query string binding', () => {
         const test = await createTest({ prop: ''}, (data: any) => `
             <div ${initComponent(data, {queryMapping: {prop: {name: 'prop'}}})}>
                 Prop: ${data.prop}
-                <button data-action="live#action" data-live-action-param="changeProp">Change prop</button>
+                <button data-action="live#action" data-action-name="changeProp">Change prop</button>
             </div>
         `);
 
@@ -165,4 +165,4 @@ describe('LiveController query string binding', () => {
 
         expectCurrentSearch().toEqual('?prop=foo');
     });
-})
+})
\ No newline at end of file
diff --git a/src/LiveComponent/assets/test/controller/render.test.ts b/src/LiveComponent/assets/test/controller/render.test.ts
index 70493eb8..df096692 100644
--- a/src/LiveComponent/assets/test/controller/render.test.ts
+++ b/src/LiveComponent/assets/test/controller/render.test.ts
@@ -149,28 +149,6 @@ describe('LiveController rendering Tests', () => {
         expect((test.element.querySelector('textarea') as HTMLTextAreaElement).value).toEqual('typing after the request starts');
     });
 
-    it('conserves cursor position of active model element', async () => {
-        const test = await createTest({ name: '' }, (data) => `
-            <div ${initComponent(data)}>
-                <input data-model="name" class="anything">
-            </div>
-        `);
-
-        test.expectsAjaxCall()
-            .expectUpdatedData({ name: 'Hello' })
-
-        const input = test.queryByDataModel('name') as HTMLInputElement;
-        userEvent.type(input, 'Hello');
-        userEvent.keyboard('{ArrowLeft}{ArrowLeft}');
-
-        await test.component.render();
-
-        // the cursor position should be preserved
-        expect(input.selectionStart).toBe(3);
-        userEvent.type(input, '!');
-        expect(input.value).toBe('Hel!lo');
-    });
-
     it('does not render over elements with data-live-ignore', async () => {
         const test = await createTest({ firstName: 'Ryan' }, (data: any) => `
             <div ${initComponent(data)}>
diff --git a/src/LiveComponent/src/Test/TestLiveComponent.php b/src/LiveComponent/src/Test/TestLiveComponent.php
index 4e9234e4..0a21f33d 100644
--- a/src/LiveComponent/src/Test/TestLiveComponent.php
+++ b/src/LiveComponent/src/Test/TestLiveComponent.php
@@ -28,14 +28,12 @@ use Symfony\UX\TwigComponent\Test\RenderedComponent;
  */
 final class TestLiveComponent
 {
-    private bool $performedInitialRequest = false;
-
     /**
      * @internal
      */
     public function __construct(
         private ComponentMetadata $metadata,
-        private array $data,
+        array $data,
         private ComponentFactory $factory,
         private KernelBrowser $client,
         private LiveComponentHydrator $hydrator,
@@ -43,7 +41,35 @@ final class TestLiveComponent
         private UrlGeneratorInterface $router,
     ) {
         $this->client->catchExceptions(false);
-        $this->data['attributes']['data-live-id'] ??= 'in-a-real-scenario-it-would-already-have-one---provide-one-yourself-if-needed';
+
+        $data['attributes']['id'] ??= 'in-a-real-scenario-it-would-already-have-one---provide-one-yourself-if-needed';
+
+        $mounted = $this->factory->create($this->metadata->getName(), $data);
+        $props = $this->hydrator->dehydrate(
+            $mounted->getComponent(),
+            $mounted->getAttributes(),
+            $this->metadataFactory->getMetadata($mounted->getName())
+        );
+
+        if ('POST' === strtoupper($this->metadata->get('method'))) {
+            $this->client->request(
+                'POST',
+                $this->router->generate($this->metadata->get('route'), [
+                    '_live_component' => $this->metadata->getName(),
+                ]),
+                [
+                    'data' => json_encode(['props' => $props->getProps()], flags: \JSON_THROW_ON_ERROR),
+                ],
+            );
+        } else {
+            $this->client->request('GET', $this->router->generate(
+                $this->metadata->get('route'),
+                [
+                    '_live_component' => $this->metadata->getName(),
+                    'props' => json_encode($props->getProps(), flags: \JSON_THROW_ON_ERROR),
+                ]
+            ));
+        }
     }
 
     public function render(): RenderedComponent
@@ -69,7 +95,6 @@ final class TestLiveComponent
      */
     public function actingAs(object $user, string $firewallContext = 'main'): self
     {
-        // we call loginUser() on the raw client in case the entire component requires authentication
         $this->client->loginUser($user, $firewallContext);
 
         return $this;
@@ -120,14 +145,14 @@ final class TestLiveComponent
 
     public function response(): Response
     {
-        return $this->client()->getResponse();
+        return $this->client->getResponse();
     }
 
     private function request(array $content = [], ?string $action = null): self
     {
         $csrfToken = $this->csrfToken();
 
-        $this->client()->request(
+        $this->client->request(
             'POST',
             $this->router->generate(
                 $this->metadata->get('route'),
@@ -145,7 +170,7 @@ final class TestLiveComponent
 
     private function props(): array
     {
-        $crawler = $this->client()->getCrawler();
+        $crawler = $this->client->getCrawler();
 
         if (!\count($node = $crawler->filter('[data-live-props-value]'))) {
             throw new \LogicException('A live component action has redirected and you can no longer access the component.');
@@ -156,7 +181,7 @@ final class TestLiveComponent
 
     private function csrfToken(): ?string
     {
-        $crawler = $this->client()->getCrawler();
+        $crawler = $this->client->getCrawler();
 
         if (!\count($node = $crawler->filter('[data-live-csrf-value]'))) {
             return null;
@@ -164,42 +189,4 @@ final class TestLiveComponent
 
         return $node->attr('data-live-csrf-value');
     }
-
-    private function client(): KernelBrowser
-    {
-        if ($this->performedInitialRequest) {
-            return $this->client;
-        }
-
-        $mounted = $this->factory->create($this->metadata->getName(), $this->data);
-        $props = $this->hydrator->dehydrate(
-            $mounted->getComponent(),
-            $mounted->getAttributes(),
-            $this->metadataFactory->getMetadata($mounted->getName())
-        );
-
-        if ('POST' === strtoupper($this->metadata->get('method'))) {
-            $this->client->request(
-                'POST',
-                $this->router->generate($this->metadata->get('route'), [
-                    '_live_component' => $this->metadata->getName(),
-                ]),
-                [
-                    'data' => json_encode(['props' => $props->getProps()], flags: \JSON_THROW_ON_ERROR),
-                ],
-            );
-        } else {
-            $this->client->request('GET', $this->router->generate(
-                $this->metadata->get('route'),
-                [
-                    '_live_component' => $this->metadata->getName(),
-                    'props' => json_encode($props->getProps(), flags: \JSON_THROW_ON_ERROR),
-                ]
-            ));
-        }
-
-        $this->performedInitialRequest = true;
-
-        return $this->client;
-    }
 }
diff --git a/src/LiveComponent/tests/Fixtures/Kernel.php b/src/LiveComponent/tests/Fixtures/Kernel.php
index 4ec25959..8cd25a20 100644
--- a/src/LiveComponent/tests/Fixtures/Kernel.php
+++ b/src/LiveComponent/tests/Fixtures/Kernel.php
@@ -16,7 +16,6 @@ use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
 use Psr\Log\NullLogger;
 use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
 use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
-use Symfony\Bundle\SecurityBundle\Security;
 use Symfony\Bundle\SecurityBundle\SecurityBundle;
 use Symfony\Bundle\TwigBundle\TwigBundle;
 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
@@ -119,19 +118,13 @@ final class Kernel extends BaseKernel
             'default_path' => '%kernel.project_dir%/tests/Fixtures/templates',
         ]);
 
-        $security = [
+        $c->extension('security', [
             'password_hashers' => [InMemoryUser::class => 'plaintext'],
             'providers' => ['users' => ['memory' => ['users' => ['kevin' => ['password' => 'pass', 'roles' => ['ROLE_USER']]]]]],
             'firewalls' => ['main' => [
                 'lazy' => true,
             ]],
-        ];
-
-        if (!class_exists(Security::class)) {
-            $security['enable_authenticator_manager'] = true;
-        }
-
-        $c->extension('security', $security);
+        ]);
 
         $c->extension('twig_component', [
             'defaults' => [
diff --git a/src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php
index dce6db3c..1e218337 100644
--- a/src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php
+++ b/src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php
@@ -15,7 +15,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\Component\DomCrawler\Crawler;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\Security\Core\User\InMemoryUser;
-use Symfony\Component\Security\Http\Attribute\IsGranted;
 use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\Entity1;
 use Symfony\UX\LiveComponent\Tests\LiveComponentTestHelper;
 use Zenstruck\Browser\Test\HasBrowser;
@@ -505,10 +504,6 @@ final class LiveComponentSubscriberTest extends KernelTestCase
 
     public function testCanHaveControllerAttributes(): void
     {
-        if (!class_exists(IsGranted::class)) {
-            $this->markTestSkipped('The security attributes are not available.');
-        }
-
         $dehydrated = $this->dehydrateComponent($this->mountComponent('with_security'));
 
         $this->browser()
diff --git a/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php b/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php
index c7914f69..bb10c4f7 100644
--- a/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php
+++ b/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php
@@ -12,9 +12,6 @@
 namespace Symfony\UX\LiveComponent\Tests\Functional\Test;
 
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
-use Symfony\Component\Security\Core\Exception\AccessDeniedException;
-use Symfony\Component\Security\Core\User\InMemoryUser;
-use Symfony\Component\Security\Http\Attribute\IsGranted;
 use Symfony\UX\LiveComponent\Test\InteractsWithLiveComponents;
 use Symfony\UX\LiveComponent\Tests\Fixtures\Component\Component2;
 
@@ -143,30 +140,4 @@ final class InteractsWithLiveComponentsTest extends KernelTestCase
 
         $testComponent->call('increase');
     }
-
-    public function testRenderingIsLazy(): void
-    {
-        if (!class_exists(IsGranted::class)) {
-            $this->markTestSkipped('The security attributes are not available.');
-        }
-
-        $testComponent = $this->createLiveComponent('with_security');
-
-        $this->expectException(AccessDeniedException::class);
-
-        $testComponent->render();
-    }
-
-    public function testActingAs(): void
-    {
-        $testComponent = $this->createLiveComponent('with_security')
-            ->actingAs(new InMemoryUser('kevin', 'pass', ['ROLE_USER']))
-        ;
-
-        $this->assertStringNotContainsString('Username: kevin', $testComponent->render());
-
-        $testComponent->call('setUsername');
-
-        $this->assertStringContainsString('Username: kevin', $testComponent->render());
-    }
 }
diff --git a/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php b/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
index d76404bc..7606dd32 100644
--- a/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
+++ b/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
@@ -1165,34 +1165,6 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
     }
 
-    public function testHydrationWithInvalidDate(): void
-    {
-        $this->expectException(BadRequestHttpException::class);
-        $this->expectExceptionMessage('The model path "createdAt" was sent invalid date data "0" or in an invalid format. Make sure it\'s a valid date and it matches the expected format "Y. m. d.".');
-
-        $this->executeHydrationTestCase(function () {
-            return HydrationTest::create(new class() {
-                #[LiveProp(writable: true, format: 'Y. m. d.')]
-                public \DateTime $createdAt;
-
-                public function __construct()
-                {
-                    $this->createdAt = new \DateTime();
-                }
-            })
-                ->mountWith([
-                    'createdAt' => new \DateTime('2023-03-05 9:23', new \DateTimeZone('America/New_York')),
-                ])
-                ->assertDehydratesTo([
-                    'createdAt' => '2023. 03. 05.',
-                ])
-                ->userUpdatesProps([
-                    'createdAt' => '0', // <-- invalid date
-                ])
-            ;
-        });
-    }
-
     public function testPassingArrayToWritablePropForHydrationIsNotAllowed(): void
     {
         $component = new class() {
diff --git a/src/LiveComponent/tests/Unit/Twig/TemplateCacheWarmerTest.php b/src/LiveComponent/tests/Unit/Twig/TemplateCacheWarmerTest.php
deleted file mode 100644
index 5e52311b..00000000
--- a/src/LiveComponent/tests/Unit/Twig/TemplateCacheWarmerTest.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-/*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\UX\LiveComponent\Tests\Unit\Twig;
-
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Cache\Adapter\NullAdapter;
-use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
-use Symfony\UX\LiveComponent\Twig\TemplateCacheWarmer;
-
-/**
- * @author Simon André <smn.andre@gmail.com>
- */
-final class TemplateCacheWarmerTest extends TestCase
-{
-    private string $cacheDir;
-    private string $cacheFile;
-    private TemplateCacheWarmer $templateCacheWarmer;
-
-    protected function setUp(): void
-    {
-        $this->cacheDir ??= sys_get_temp_dir();
-        $this->cacheFile ??= $this->cacheDir.'/cache_file';
-        if (file_exists($this->cacheFile)) {
-            unlink($this->cacheFile);
-        }
-        $this->templateCacheWarmer ??= new TemplateCacheWarmer(
-            new \ArrayObject(['template1', 'template2']),
-            'cache_file',
-            'secret'
-        );
-    }
-
-    public function testWarmUpCreatesCacheFile(): void
-    {
-        $this->assertFileDoesNotExist($this->cacheFile);
-
-        $this->templateCacheWarmer->warmUp($this->cacheDir);
-
-        $this->assertFileExists($this->cacheFile);
-    }
-
-    public function testWarmUpCreatesCorrectCacheContent(): void
-    {
-        $this->templateCacheWarmer->warmUp($this->cacheDir);
-        $adapter = new PhpArrayAdapter($this->cacheFile, new NullAdapter());
-        $item = $adapter->getItem('map');
-
-        $this->assertSame(
-            [
-                hash('xxh128', 'template1secret') => 'template1',
-                hash('xxh128', 'template2secret') => 'template2',
-            ],
-            $item->get()
-        );
-    }
-
-    public function testWarmUpCreatesReproductibleTemplateMap(): void
-    {
-        $this->templateCacheWarmer->warmUp($this->cacheDir);
-        $adapter = new PhpArrayAdapter($this->cacheFile, new NullAdapter());
-        $map1 = $adapter->getItem('map')->get();
-
-        $this->templateCacheWarmer->warmUp($this->cacheDir);
-        $adapter = new PhpArrayAdapter($this->cacheFile, new NullAdapter());
-        $map2 = $adapter->getItem('map')->get();
-
-        $this->assertSame($map1, $map2);
-    }
-}
diff --git a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php
index aa61825e..ee09fca9 100644
--- a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php
+++ b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php
@@ -14,6 +14,7 @@ namespace Symfony\UX\TwigComponent\Tests\Integration;
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\UX\TwigComponent\Tests\Fixtures\User;
 use Twig\Environment;
+use Twig\Error\RuntimeError;
 
 /**
  * @author Kevin Bond <kevinbond@gmail.com>
@@ -62,7 +63,11 @@ final class ComponentExtensionTest extends KernelTestCase
 
     public function testCanNotRenderComponentWithInvalidExpressions(): void
     {
-        $this->expectException(\Throwable::class);
+        if (Environment::MAJOR_VERSION === 2) {
+            $this->expectException(\TypeError::class);
+        } else {
+            $this->expectException(RuntimeError::class);
+        }
 
         self::getContainer()->get(Environment::class)->render('invalid_flexible_component.html.twig');
     }
diff --git a/ux.symfony.com/tests/Unit/Util/SourceCleanerTest.php b/ux.symfony.com/tests/Unit/Util/SourceCleanerTest.php
index 3db175c4..f76bd07e 100644
--- a/ux.symfony.com/tests/Unit/Util/SourceCleanerTest.php
+++ b/ux.symfony.com/tests/Unit/Util/SourceCleanerTest.php
@@ -109,19 +109,19 @@ class SourceCleanerTest extends TestCase
     public function testItRemovesExcessHtml(): void
     {
         $input = <<<EOF
-            <div class="p-4 markdown-container shadow-blur shadow-blur--rainbow mt-5 row" data-controller="markdown">
+            <div class="p-4 markdown-container shadow-blur shadow-blur--rainbow mt-5 row" {{ stimulus_controller('markdown') }}>
                 <div class="col-12 col-md-5">
                     <textarea rows="3" class="form-control" aria-label="Type markdown into this box"
-                        data-markdown-target="input"
+                        {{ stimulus_target('markdown', 'input') }}
                     >Writing JavaScript is a **dream** with Stimulus 🤩</textarea>
                 </div>
                 <div class="col-12 col-md-2 text-center">
-                    <button class="btn btn-sm btn-dark mt-3" data-action="markdown#render">
+                    <button class="btn btn-sm btn-dark mt-3" {{ stimulus_action('markdown', 'render') }}>
                         Convert <i class="fa fa-arrow-right"></i>
                     </button>
                 </div>
                 <div class="col-12 col-md-5 mt-3 mt-md-0">
-                    <div style="min-height: 86px;" class="markdown-form-render-container p-2" data-markdown-target="preview">
+                    <div style="min-height: 86px;" class="markdown-form-render-container p-2" {{ stimulus_target('markdown', 'preview') }} >
                         <small class="fw-light">(click "Convert")</small>
                     </div>
                 </div>
@@ -129,14 +129,14 @@ class SourceCleanerTest extends TestCase
             EOF;
 
         $expected = <<<EOF
-            <div data-controller="markdown">
+            <div {{ stimulus_controller('markdown') }}>
                 <textarea
-                    data-markdown-target="input"
+                    {{ stimulus_target('markdown', 'input') }}
                 >Writing JavaScript is a **dream** with Stimulus 🤩</textarea>
-                <button data-action="markdown#render">
+                <button {{ stimulus_action('markdown', 'render') }}>
                     Convert <i></i>
                 </button>
-                <div data-markdown-target="preview">
+                <div {{ stimulus_target('markdown', 'preview') }} >
                     <small>(click "Convert")</small>
                 </div>
             </div>