Repository: symfony/ux
Pull request #1519 (2024-02-18 22:20:47)

[Translator] Clean code to suppress warnings
I've had enough of those TS warnings 😅 

```console
/home/runner/work/ux/ux/src/Translator/assets/src/formatters/formatter.ts
  82:38   warning  Forbidden non-null assertion  @typescript-eslint/no-non-null-assertion
  86:47   warning  Forbidden non-null assertion  @typescript-eslint/no-non-null-assertion
  86:107  warning  Forbidden non-null assertion  @typescript-eslint/no-non-null-assertion
  87:62   warning  Forbidden non-null assertion  @typescript-eslint/no-non-null-assertion
  87:124  warning  Forbidden non-null assertion  @typescript-eslint/no-non-null-assertion
  89:30   warning  Forbidden non-null assertion  @typescript-eslint/no-non-null-assertion
  90:33   warning  Forbidden non-null assertion  @typescript-eslint/no-non-null-assertion
  92:34   warning  Forbidden non-null assertion  @typescript-eslint/no-non-null-assertion

/home/runner/work/ux/ux/src/Translator/assets/src/translator.ts
  28:26  warning  'Translations' is defined but never used  @typescript-eslint/no-unused-vars
```

(ex: https://github.com/symfony/ux/actions/runs/7951722335/job/21705480408)

--

The tests are green, but if someone want to checkproof ? 

weaverryan: @Kocal does this look good to you?

Patch:
diff --git a/src/Autocomplete/CHANGELOG.md b/src/Autocomplete/CHANGELOG.md
index f4a3c44a..5b78d7ec 100644
--- a/src/Autocomplete/CHANGELOG.md
+++ b/src/Autocomplete/CHANGELOG.md
@@ -1,11 +1,8 @@
 # CHANGELOG
 
-## 2.15.0
+## Unreleased
 
--   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
+-   Add doctrine/orm 3 support.
 
 ## 2.14.0
 
diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js
index f7536c00..5659d9bc 100644
--- a/src/Autocomplete/assets/dist/controller.js
+++ b/src/Autocomplete/assets/dist/controller.js
@@ -199,13 +199,6 @@ 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 === '');
-        if (originalPlaceholderOption &&
-            newPlaceholderOption &&
-            originalPlaceholderOption.text !== newPlaceholderOption.text) {
-            return false;
-        }
         if (filteredOriginalOptions.length !== filteredNewOptions.length) {
             return false;
         }
diff --git a/src/Autocomplete/assets/src/controller.ts b/src/Autocomplete/assets/src/controller.ts
index 8be04e8a..bc3812b7 100644
--- a/src/Autocomplete/assets/src/controller.ts
+++ b/src/Autocomplete/assets/src/controller.ts
@@ -429,17 +429,6 @@ export default class extends Controller {
         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 === '');
-
-        if (
-            originalPlaceholderOption &&
-            newPlaceholderOption &&
-            originalPlaceholderOption.text !== newPlaceholderOption.text
-        ) {
-            return false;
-        }
-
         if (filteredOriginalOptions.length !== filteredNewOptions.length) {
             return false;
         }
diff --git a/src/Autocomplete/translations/AutocompleteBundle.ar.php b/src/Autocomplete/translations/AutocompleteBundle.ar.php
index b8ced4cb..9d787cab 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.ar.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.ar.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'تحميل المزيد من النتائج...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'لم يتم العثور على أي نتائج',
     'No more results' => 'لا توجد نتائج أٌخرى',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.bg.php b/src/Autocomplete/translations/AutocompleteBundle.bg.php
index e16b6aea..a700a3e7 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.bg.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.bg.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Зареждане на още резултати...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Няма намерени съвпадения',
     'No more results' => 'Няма повече резултати',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.ca.php b/src/Autocomplete/translations/AutocompleteBundle.ca.php
index 4b0ddaaa..419e764a 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.ca.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.ca.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'S\'estan carregant més resultats...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'No s\'han trobat resultats',
     'No more results' => 'No hi ha més resultats',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.cs.php b/src/Autocomplete/translations/AutocompleteBundle.cs.php
index c90cbcf4..fba7b1ac 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.cs.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.cs.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Načítání dalších výsledků...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Nenalezeny žádné položky',
-    'No more results' => 'Žádné další výsledky',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.da.php b/src/Autocomplete/translations/AutocompleteBundle.da.php
index d77714f9..ae810fe7 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.da.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.da.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Indlæser flere resultater...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Ingen resultater fundet',
-    'No more results' => 'Ingen flere resultater',
+    // 'No more results' => 'No more results',
 ];
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/Autocomplete/translations/AutocompleteBundle.el.php b/src/Autocomplete/translations/AutocompleteBundle.el.php
index 6ded7859..09842d19 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.el.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.el.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Φόρτωση περισσότερων αποτελεσμάτων...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Δεν βρέθηκαν αποτελέσματα',
-    'No more results' => 'Δεν υπάρχουν άλλα αποτελέσματα',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.es.php b/src/Autocomplete/translations/AutocompleteBundle.es.php
index 6d7345ab..1606d614 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.es.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.es.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Cargando más resultados...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'No se han encontrado resultados',
     'No more results' => 'No hay más resultados',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.eu.php b/src/Autocomplete/translations/AutocompleteBundle.eu.php
index 98cb18d7..14ac876f 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.eu.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.eu.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Emaitza gehiago kargatzen...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Ez da bat datorrenik aurkitu',
-    'No more results' => 'Ez dago emaitza gehiagorik',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.fa.php b/src/Autocomplete/translations/AutocompleteBundle.fa.php
index 06db64f4..8209bc4b 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.fa.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.fa.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'در حال بارگذاری نتایج بیشتر...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'هیچ نتیجه‌ای یافت نشد',
     'No more results' => 'نتیجه دیگری وجود ندارد',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.fi.php b/src/Autocomplete/translations/AutocompleteBundle.fi.php
index de081b9f..4a55386e 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.fi.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.fi.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Ladataan lisää tuloksia...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Ei tuloksia',
-    'No more results' => 'Ei enempää tuloksia',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.gl.php b/src/Autocomplete/translations/AutocompleteBundle.gl.php
index 76975202..30572137 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.gl.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.gl.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Cargando máis resultados...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Non se atoparon resultados',
-    'No more results' => 'Non hai máis resultados',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.hr.php b/src/Autocomplete/translations/AutocompleteBundle.hr.php
index 57f7aeca..1da9a49f 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.hr.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.hr.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Učitavanje više rezultata...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Nema rezultata',
-    'No more results' => 'Nema više rezultata',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.id.php b/src/Autocomplete/translations/AutocompleteBundle.id.php
index 8ca5f81f..255d755e 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.id.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.id.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Memuat hasil lebih banyak...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Tidak ada hasil yang ditemukan',
     'No more results' => 'Tidak ada hasil lagi',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.it.php b/src/Autocomplete/translations/AutocompleteBundle.it.php
index 815238b1..3e3a115e 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.it.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.it.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Caricamento di altri risultati...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Nessun risultato trovato',
     'No more results' => 'Non ci sono altri risultati',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.lb.php b/src/Autocomplete/translations/AutocompleteBundle.lb.php
index 79be23c3..c1ce1bad 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.lb.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.lb.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Lueden méi Resultater...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Keng Resultater fonnt',
     'No more results' => 'Keng weider Resultater',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.lt.php b/src/Autocomplete/translations/AutocompleteBundle.lt.php
index ed464e45..1e73aeaa 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.lt.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.lt.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Kraunama daugiau rezultatų...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Atitikmenų nerasta',
-    'No more results' => 'Daugiau rezultatų nėra',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.pl.php b/src/Autocomplete/translations/AutocompleteBundle.pl.php
index a5f8b2c2..3625ce5e 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.pl.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.pl.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Wczytywanie więcej wyników...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Brak wyników',
-    'No more results' => 'Brak więcej wyników',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.pt.php b/src/Autocomplete/translations/AutocompleteBundle.pt.php
index bd8d827f..2afd3b91 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.pt.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.pt.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Carregando mais resultados...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Sem resultados',
-    'No more results' => 'Não há mais resultados',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.pt_BR.php b/src/Autocomplete/translations/AutocompleteBundle.pt_BR.php
index 4e136e00..44f780e8 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.pt_BR.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.pt_BR.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Carregando mais resultados...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Nenhum resultado encontrado',
-    'No more results' => 'Não há mais resultados',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.ro.php b/src/Autocomplete/translations/AutocompleteBundle.ro.php
index 7eabfd85..07697f87 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.ro.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.ro.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Se încarcă mai multe rezultate...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Nu au fost găsite rezultate',
-    'No more results' => 'Nu mai sunt alte rezultate',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.ru.php b/src/Autocomplete/translations/AutocompleteBundle.ru.php
index 1277f42f..37da5677 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.ru.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.ru.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Загрузка дополнительных результатов...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Совпадений не найдено',
-    'No more results' => 'Больше результатов нет',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.sl.php b/src/Autocomplete/translations/AutocompleteBundle.sl.php
index 39437a01..15835995 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.sl.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.sl.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Nalaganje več rezultatov...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Ni zadetkov',
-    'No more results' => 'Ni več rezultatov',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.sr_RS.php b/src/Autocomplete/translations/AutocompleteBundle.sr_RS.php
index efeafae2..21a631f7 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.sr_RS.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.sr_RS.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Учитавање још резултата...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Nema rezultata',
     'No more results' => 'Nema više rezultata',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.sv.php b/src/Autocomplete/translations/AutocompleteBundle.sv.php
index 918db3d8..c4b76c81 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.sv.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.sv.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Laddar fler resultat...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Inga träffar',
-    'No more results' => 'Inga fler resultat',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.tr.php b/src/Autocomplete/translations/AutocompleteBundle.tr.php
index f8d1776f..7204a365 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.tr.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.tr.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Daha fazla sonuç yükleniyor...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Sonuç bulunamadı',
-    'No more results' => 'Başka sonuç yok',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.uk.php b/src/Autocomplete/translations/AutocompleteBundle.uk.php
index 365114c1..86ace1e4 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.uk.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.uk.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => 'Завантаження додаткових результатів...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => 'Нічого не знайдено',
-    'No more results' => 'Більше результатів немає',
+    // 'No more results' => 'No more results',
 ];
diff --git a/src/Autocomplete/translations/AutocompleteBundle.zh_CN.php b/src/Autocomplete/translations/AutocompleteBundle.zh_CN.php
index 7a969be6..f36d16dc 100644
--- a/src/Autocomplete/translations/AutocompleteBundle.zh_CN.php
+++ b/src/Autocomplete/translations/AutocompleteBundle.zh_CN.php
@@ -10,7 +10,7 @@
  */
 
 return [
-    'Loading more results...' => '加载更多结果...',
+    // 'Loading more results...' => 'Loading more results...',
     'No results found' => '未找到结果',
-    'No more results' => '没有更多结果',
+    // 'No more results' => 'No more results',
 ];
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 d154b862..9cd0e5dc 100644
--- a/src/Chartjs/assets/package.json
+++ b/src/Chartjs/assets/package.json
@@ -21,12 +21,12 @@
     },
     "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",
         "@types/chart.js": "^2.9.34",
-        "chart.js": "^3.4.1 || ^4.0",
+        "chart.js": "^3.4.1 <3.9 || ^4.0",
         "resize-observer-polyfill": "^1.5.1",
         "vitest-canvas-mock": "^0.3.3"
     }
diff --git a/src/LiveComponent/CHANGELOG.md b/src/LiveComponent/CHANGELOG.md
index 169366ae..8903ea2e 100644
--- a/src/LiveComponent/CHANGELOG.md
+++ b/src/LiveComponent/CHANGELOG.md
@@ -1,60 +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"`.
-
--   Reverted setting `ignoreActiveValue: true` in Idiomorph
-
 ## 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 1b106871..7e5e4b78 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'));
@@ -1349,6 +1378,7 @@ 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)) {
@@ -1374,11 +1404,6 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
                     if (modifiedFieldElements.includes(fromEl)) {
                         setValueOnElement(toEl, getElementValue(fromEl));
                     }
-                    if (fromEl === document.activeElement
-                        && fromEl !== document.body
-                        && null !== getModelDirectiveFromElement(fromEl, false)) {
-                        setValueOnElement(toEl, getElementValue(fromEl));
-                    }
                     const elementChanges = externalMutationTracker.getChangedElement(fromEl);
                     if (elementChanges) {
                         elementChanges.applyToElement(toEl);
@@ -2917,18 +2942,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();
             });
@@ -2963,15 +2985,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);
@@ -2987,6 +3006,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);
     }
@@ -2997,13 +3019,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) => {
@@ -3019,7 +3039,7 @@ class LiveControllerDefault extends Controller {
             });
             emits.push({
                 name: directive.action,
-                data: eventArgs,
+                data: directive.named,
                 nameMatch,
             });
         });
diff --git a/src/LiveComponent/assets/src/Component/UnsyncedInputsTracker.ts b/src/LiveComponent/assets/src/Component/UnsyncedInputsTracker.ts
index f56331c6..a13a2065 100644
--- a/src/LiveComponent/assets/src/Component/UnsyncedInputsTracker.ts
+++ b/src/LiveComponent/assets/src/Component/UnsyncedInputsTracker.ts
@@ -73,7 +73,7 @@ export default class {
 /**
  * Tracks field & models whose values are "unsynced".
  *
- * For a model, unsynced means that the value has been updated inside
+ * For a model, unsynced means that the value has been updated inside of
  * a field (e.g. an input), but that this new value hasn't
  * yet been set onto the actual model data. It is "unsynced"
  * from the underlying model data.
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 534ba95a..92a31080 100644
--- a/src/LiveComponent/assets/src/morphdom.ts
+++ b/src/LiveComponent/assets/src/morphdom.ts
@@ -1,4 +1,4 @@
-import { cloneHTMLElement, getModelDirectiveFromElement, setValueOnElement } from './dom_utils';
+import { cloneHTMLElement, setValueOnElement } from './dom_utils';
 // @ts-ignore
 import { Idiomorph } from 'idiomorph/dist/idiomorph.esm.js';
 import { normalizeAttributesForComparison } from './normalize_attributes_for_comparison';
@@ -53,6 +53,12 @@ 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
@@ -100,23 +106,6 @@ export function executeMorphdom(
                         setValueOnElement(toEl, getElementValue(fromEl));
                     }
 
-                    // Special handling for the active element of a model field.
-                    // Make the "to" element match the "from" element's value
-                    // to avoid any value change during the morphing. After morphing,
-                    // the SetValuesOntoModelFieldsPlugin handles setting the value
-                    // to whatever is in the data store.
-                    // Avoiding changing the value during morphing is important
-                    // to maintain the cursor position.
-                    // We skip this for non-model elements and allow this to either
-                    // maintain the value if changed (see code above) or for the
-                    // morphing process to update it to the value from the server.
-                    if (fromEl === document.activeElement
-                        && fromEl !== document.body
-                        && null !== getModelDirectiveFromElement(fromEl, false)
-                    ) {
-                        setValueOnElement(toEl, getElementValue(fromEl));
-                    }
-
                     // handle any external changes to this element
                     const elementChanges = externalMutationTracker.getChangedElement(fromEl);
                     if (elementChanges) {
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 70a0bb39..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-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-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>
 
@@ -1940,7 +1931,7 @@ Inline rendering:
             {% endfor %}
 
             {# render the add button #}
-            {{ form_widget(form.comments.vars.button_add, { label: '+ Add comment', attr: { class: 'btn btn-outline-primary' } }) }}
+            {{ form_widget(form.comments.vars.button_add, { label: '+ Add comment', class: 'btn btn-outline-primary' }) }}
 
             {# render rest of the form #}
             {{ form_row(form) }}
@@ -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
@@ -2242,12 +2233,8 @@ Deferring / Lazy Loading Components
 If a component is heavy to render, you can defer rendering it until after
 the page has loaded. To do this, add the ``defer`` option:
 
-.. code-block:: html+twig
-
-    {# With the HTML syntax #}
-    <twig:SomeHeavyComponent defer />
+.. code-block:: twig
 
-    {# With the component function #}
     {{ component('SomeHeavyComponent', { defer: true }) }}
 
 This renders an empty ``<div>`` tag, but triggers an Ajax call to render the
@@ -2259,81 +2246,20 @@ real component once the page has loaded.
     page load, but it isn't rendered. So keep your heavy work to methods in
     your component (e.g. ``getProducts()``) that are only called when rendering.
 
-You can define some content to be rendered while the component is loading, either
-inside the component template (the ``placeholder`` macro) or from the calling template
-(the ``loading-template`` attribute and the ``loadingContent`` block).
-
-.. versionadded:: 2.17.0
-
-    Defining a placeholder macro into the component template was added in Live Components 2.17.0.
-
-In the component template, define a ``placeholder`` macro, outside of the
-component's main content. This macro will be called when the component is deferred:
-
-.. code-block:: html+twig
-
-    {# templates/recommended-products.html.twig #}
-    <div {{ attributes }}>
-        {# This will be rendered when the component is fully loaded #}
-        {% for product in this.products %}
-            <div>{{ product.name }}</div>
-        {% endfor %}
-    </div>
-
-    {% macro placeholder(props) %}
-        {# This content will (only) be rendered as loading content #}
-        <span class="loading-row"></span>
-    {% endmacro %}
-
-The ``props`` argument contains the props passed to the component.
-You can use it to customize the placeholder content. Let's say your
-component shows a certain number of products (defined with the ``size``
-prop). You can use it to define a placeholder that shows the same
-number of rows:
-
-.. code-block:: html+twig
-
-    {# In the calling template #}
-    <twig:RecommendedProducts size="3" defer />
-
-.. code-block:: html+twig
-
-    {# In the component template #}
-    {% macro placeholder(props) %}
-        {% for i in 1..props.size %}
-            <div class="loading-product">
-                ...
-            </div>
-        {% endfor %}
-    {% endmacro %}
-
-To customize the loading content from the calling template, you can use
-the ``loading-template`` option to point to a template:
+To add some loading text before the real component is loaded, use the
+``loading-template`` option to point to a template:
 
-.. code-block:: html+twig
-
-    {# With the HTML syntax #}
-    <twig:SomeHeavyComponent defer loading-template="spinning-wheel.html.twig" />
+.. code-block:: twig
 
-    {# With the component function #}
     {{ component('SomeHeavyComponent', { defer: true, loading-template: 'spinning-wheel.html.twig' }) }}
 
 Or override the ``loadingContent`` block:
 
-.. code-block:: html+twig
-
-    {# With the HTML syntax #}
-    <twig:SomeHeavyComponent defer>
-        <twig:block name="loadingContent">Custom Loading Content...</twig:block>
-    </twig:SomeHeavyComponent>
+.. code-block:: twig
 
-    {# With the component tag #}
-    {% component SomeHeavyComponent with { defer: true } %}
+    {% component SomeHeavyComponent with { defer: true }) }}
         {% block loadingContent %}Loading...{% endblock %}
-    {% endcomponent %}
-
-When ``loading-template`` or ``loadingContent`` is defined, the ``placeholder``
-macro is ignored.
+    {{ end_component() }}
 
 To change the initial tag from a ``div`` to something else, use the ``loading-tag`` option:
 
@@ -2521,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``::
@@ -2629,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::
@@ -2646,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::
@@ -2662,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::
@@ -2845,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
@@ -3009,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>
@@ -3527,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
@@ -3535,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
@@ -3545,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/src/DependencyInjection/LiveComponentExtension.php b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
index 06e61ea9..8683214a 100644
--- a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
+++ b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
@@ -253,7 +253,7 @@ final class LiveComponentExtension extends Extension implements PrependExtension
         ;
 
         $container->register('ux.live_component.twig.template_mapper', TemplateMap::class)
-            ->setArguments(['%kernel.build_dir%/'.self::TEMPLATES_MAP_FILENAME]);
+            ->setArguments(['%kernel.cache_dir%/'.self::TEMPLATES_MAP_FILENAME]);
 
         $container->register('ux.live_component.twig.cache_warmer', TemplateCacheWarmer::class)
             ->setArguments([
diff --git a/src/LiveComponent/src/EventListener/DeferLiveComponentSubscriber.php b/src/LiveComponent/src/EventListener/DeferLiveComponentSubscriber.php
index d7e598cf..19928e8b 100644
--- a/src/LiveComponent/src/EventListener/DeferLiveComponentSubscriber.php
+++ b/src/LiveComponent/src/EventListener/DeferLiveComponentSubscriber.php
@@ -52,7 +52,6 @@ final class DeferLiveComponentSubscriber implements EventSubscriberInterface
             return;
         }
 
-        $componentTemplate = $event->getMetadata()->getTemplate();
         $event->setTemplate('@LiveComponent/deferred.html.twig');
 
         $variables = $event->getVariables();
@@ -67,8 +66,6 @@ final class DeferLiveComponentSubscriber implements EventSubscriberInterface
             $variables['loadingTag'] = $mountedComponent->getExtraMetadata('loading-tag');
         }
 
-        $variables['componentTemplate'] = $componentTemplate;
-
         $event->setVariables($variables);
     }
 
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 3e6f4e9a..3c9d18ac 100644
--- a/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
+++ b/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
@@ -36,8 +36,7 @@ final class TemplateCacheWarmer implements CacheWarmerInterface
             $map[hash('xxh128', $item.$this->secret)] = $item;
         }
 
-        $cacheFile = sprintf('%s%s%s', $buildDir ?? $cacheDir, \DIRECTORY_SEPARATOR, $this->cacheFilename);
-        PhpArrayAdapter::create($cacheFile, new NullAdapter())->warmUp(['map' => $map]);
+        (new PhpArrayAdapter($cacheDir.'/'.$this->cacheFilename, new NullAdapter()))->warmUp(['map' => $map]);
 
         return [];
     }
diff --git a/src/LiveComponent/src/Twig/TemplateMap.php b/src/LiveComponent/src/Twig/TemplateMap.php
index 142e58cd..03dd2ffd 100644
--- a/src/LiveComponent/src/Twig/TemplateMap.php
+++ b/src/LiveComponent/src/Twig/TemplateMap.php
@@ -21,14 +21,11 @@ use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
  */
 final class TemplateMap
 {
-    /**
-     * @var array<string, string> Map of <obscured name> => <template name>
-     */
     private readonly array $map;
 
     public function __construct(string $cacheFile)
     {
-        $this->map = PhpArrayAdapter::create($cacheFile, new NullAdapter())->getItem('map')->get();
+        $this->map = (new PhpArrayAdapter($cacheFile, new NullAdapter()))->getItem('map')->get();
     }
 
     public function resolve(string $obscuredName): string
@@ -38,7 +35,8 @@ final class TemplateMap
 
     public function obscuredName(string $templateName): string
     {
-        if (false === $obscuredName = array_search($templateName, $this->map, true)) {
+        $obscuredName = array_search($templateName, $this->map, true);
+        if (false === $obscuredName) {
             throw new \RuntimeException(sprintf('Cannot find a match for template "%s". Cache may be corrupt.', $templateName));
         }
 
diff --git a/src/LiveComponent/templates/deferred.html.twig b/src/LiveComponent/templates/deferred.html.twig
index b55ec440..0705be8e 100644
--- a/src/LiveComponent/templates/deferred.html.twig
+++ b/src/LiveComponent/templates/deferred.html.twig
@@ -2,11 +2,6 @@
     {% block loadingContent %}
         {% if loadingTemplate != null %}
             {{ include(loadingTemplate) }}
-        {% else %}
-            {% from componentTemplate import placeholder %}
-            {% if placeholder is defined %}
-                {{ placeholder(__props|default) }}
-            {% endif %}
         {% endif %}
     {% endblock %}
 </{{ loadingTag }}>
diff --git a/src/StimulusBundle/src/AssetMapper/AutoImportLocator.php b/src/StimulusBundle/src/AssetMapper/AutoImportLocator.php
index 06dc7842..a5dafe59 100644
--- a/src/StimulusBundle/src/AssetMapper/AutoImportLocator.php
+++ b/src/StimulusBundle/src/AssetMapper/AutoImportLocator.php
@@ -64,7 +64,7 @@ class AutoImportLocator
 
         $entry = $this->importMapConfigReader->findRootImportMapEntry($path);
         if (!$entry) {
-            throw new \LogicException(sprintf('The autoimport "%s" could not be found in importmap.php. Try running "php bin/console importmap:require %s".', $path, $path));
+            throw new \LogicException(sprintf('The autoimport "%s" could not be found in importmap.php. Try running "importmap:require %s".', $path, $path));
         }
 
         return new MappedControllerAutoImport($path, true);
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/dist/translator_controller.js b/src/Translator/assets/dist/translator_controller.js
index d4f3317a..a92243af 100644
--- a/src/Translator/assets/dist/translator_controller.js
+++ b/src/Translator/assets/dist/translator_controller.js
@@ -38,41 +38,34 @@ function format(id, parameters = {}, locale) {
         parts = id.split('|');
     }
     else {
-        const matches = id.match(/(?:\|\||[^|])+/g);
-        if (matches !== null) {
-            parts = matches;
-        }
+        parts = id.match(/(?:\|\||[^|])+/g) || [];
     }
     const intervalRegex = /^(?<interval>({\s*(-?\d+(\.\d+)?[\s*,\s*\-?\d+(.\d+)?]*)\s*})|(?<left_delimiter>[[\]])\s*(?<left>-Inf|-?\d+(\.\d+)?)\s*,\s*(?<right>\+?Inf|-?\d+(\.\d+)?)\s*(?<right_delimiter>[[\]]))\s*(?<message>.*?)$/s;
     const standardRules = [];
     for (let part of parts) {
         part = part.trim().replace(/\|\|/g, '|');
-        let matches = part.match(intervalRegex);
-        if (matches !== null) {
+        const matches = part.match(intervalRegex);
+        if (matches) {
+            const matchGroups = matches.groups || {};
             if (matches[2]) {
                 for (const n of matches[3].split(',')) {
                     if (number === Number(n)) {
-                        return strtr(matches.groups['message'], parameters);
+                        return strtr(matchGroups.message, parameters);
                     }
                 }
             }
             else {
-                const leftNumber = '-Inf' === matches.groups['left'] ? Number.NEGATIVE_INFINITY : Number(matches.groups['left']);
-                const rightNumber = ['Inf', '+Inf'].includes(matches.groups['right']) ? Number.POSITIVE_INFINITY : Number(matches.groups['right']);
-                if (('[' === matches.groups['left_delimiter'] ? number >= leftNumber : number > leftNumber)
-                    && (']' === matches.groups['right_delimiter'] ? number <= rightNumber : number < rightNumber)) {
-                    return strtr(matches.groups['message'], parameters);
+                const leftNumber = '-Inf' === matchGroups.left ? Number.NEGATIVE_INFINITY : Number(matchGroups.left);
+                const rightNumber = ['Inf', '+Inf'].includes(matchGroups.right) ? Number.POSITIVE_INFINITY : Number(matchGroups.right);
+                if (('[' === matchGroups.left_delimiter ? number >= leftNumber : number > leftNumber)
+                    && (']' === matchGroups.right_delimiter ? number <= rightNumber : number < rightNumber)) {
+                    return strtr(matchGroups.message, parameters);
                 }
             }
         }
         else {
-            matches = part.match(/^\w+:\s*(.*?)$/);
-            if (matches !== null) {
-                standardRules.push(matches[1]);
-            }
-            else {
-                standardRules.push(part);
-            }
+            const ruleMatch = part.match(/^\w+:\s*(.*?)$/);
+            standardRules.push(ruleMatch ? ruleMatch[1] : part);
         }
     }
     const position = getPluralizationRule(number, locale);
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/formatter.ts b/src/Translator/assets/src/formatters/formatter.ts
index e50305ab..590a4a0c 100644
--- a/src/Translator/assets/src/formatters/formatter.ts
+++ b/src/Translator/assets/src/formatters/formatter.ts
@@ -62,10 +62,7 @@ export function format(id: string, parameters: Record<string, string | number> =
     if (/^\|+$/.test(id)) {
         parts = id.split('|');
     } else {
-        const matches = id.match(/(?:\|\||[^|])+/g);
-        if (matches !== null) {
-            parts = matches;
-        }
+        parts = id.match(/(?:\|\||[^|])+/g) || [];
     }
 
     const intervalRegex = /^(?<interval>({\s*(-?\d+(\.\d+)?[\s*,\s*\-?\d+(.\d+)?]*)\s*})|(?<left_delimiter>[[\]])\s*(?<left>-Inf|-?\d+(\.\d+)?)\s*,\s*(?<right>\+?Inf|-?\d+(\.\d+)?)\s*(?<right_delimiter>[[\]]))\s*(?<message>.*?)$/s;
@@ -74,31 +71,31 @@ export function format(id: string, parameters: Record<string, string | number> =
     for (let part of parts) {
         part = part.trim().replace(/\|\|/g, '|');
 
-        let matches = part.match(intervalRegex);
-        if (matches !== null) {
+        const matches = part.match(intervalRegex);
+        if (matches) {
+            /**
+             * @type {interval: string, left_delimiter: string, left: string, right_delimiter: string, right: string, message: string}
+             */
+            const matchGroups: { [p: string]: string } = matches.groups || {};
             if (matches[2]) {
                 for (const n of matches[3].split(',')) {
                     if (number === Number(n)) {
-                        return strtr(matches.groups!['message'], parameters);
+                        return strtr(matchGroups.message, parameters);
                     }
                 }
             } else {
-                const leftNumber = '-Inf' === matches.groups!['left'] ? Number.NEGATIVE_INFINITY : Number(matches.groups!['left']);
-                const rightNumber = ['Inf', '+Inf'].includes(matches.groups!['right']) ? Number.POSITIVE_INFINITY : Number(matches.groups!['right']);
+                const leftNumber = '-Inf' === matchGroups.left ? Number.NEGATIVE_INFINITY : Number(matchGroups.left);
+                const rightNumber = ['Inf', '+Inf'].includes(matchGroups.right) ? Number.POSITIVE_INFINITY : Number(matchGroups.right);
 
-                if (('[' === matches.groups!['left_delimiter'] ? number >= leftNumber : number > leftNumber)
-                    && (']' === matches.groups!['right_delimiter'] ? number <= rightNumber : number < rightNumber)
+                if (('[' === matchGroups.left_delimiter ? number >= leftNumber : number > leftNumber)
+                    && (']' === matchGroups.right_delimiter ? number <= rightNumber : number < rightNumber)
                 ) {
-                    return strtr(matches.groups!['message'], parameters);
+                    return strtr(matchGroups.message, parameters);
                 }
             }
         } else {
-            matches = part.match(/^\w+:\s*(.*?)$/);
-            if (matches !== null) {
-                standardRules.push(matches[1]);
-            } else {
-                standardRules.push(part);
-            }
+            const ruleMatch = part.match(/^\w+:\s*(.*?)$/);
+            standardRules.push(ruleMatch ? ruleMatch[1] : part);
         }
     }
 
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/Translator/assets/src/translator.ts b/src/Translator/assets/src/translator.ts
index 903bac46..8103ccad 100644
--- a/src/Translator/assets/src/translator.ts
+++ b/src/Translator/assets/src/translator.ts
@@ -25,6 +25,7 @@ export type ParametersOf<M, D extends DomainType> = M extends Message<infer Tran
         : never
     : never;
 
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 export interface Message<Translations extends TranslationsType, Locale extends LocaleType> {
     id: string;
     translations: {
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/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/styles/demos/_live-memory.scss b/ux.symfony.com/assets/styles/demos/_live-memory.scss
new file mode 100644
index 00000000..ba03970c
--- /dev/null
+++ b/ux.symfony.com/assets/styles/demos/_live-memory.scss
@@ -0,0 +1,25 @@
+// -----------------------------------------------------------------
+// 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 db51ecfd..747561dd 100644
--- a/ux.symfony.com/assets/styles/demos/live-memory.min.css
+++ b/ux.symfony.com/assets/styles/demos/live-memory.min.css
@@ -1,26 +1,992 @@
-/** -----------------------------------------------------------------
- * LiveMemory demo
- * --------------------------------------------------------------- */
+*, *::before, *::after {
+  box-sizing: border-box;
+}
 
-@import "live-memory/app/reset";
-@import "live-memory/app/root";
-@import "live-memory/app/html";
+* {
+  margin: 0;
+  padding: 0;
+}
 
-@import "live-memory/base/header";
+body {
+  line-height: 1.5;
+  -webkit-font-smoothing: antialiased;
+}
 
-@import "../components/Icon";
-@import "live-memory/components/Board";
-@import "live-memory/components/Card";
+img, picture, video, canvas, svg {
+  display: block;
+  max-width: 100%;
+}
 
-@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";
+input, button, textarea, select {
+  font: inherit;
+  background: none;
+  color: inherit;
+  border: 0;
+  padding: 0;
+}
 
-@import "../utilities/background";
-@import "../utilities/shadow";
+#root, #__next {
+  isolation: isolate;
+}
 
-@import "live-memory/app/themes";
+: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);
+}
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 f0175923..7be9f4a5 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,6 +110,9 @@ 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;
@@ -117,11 +120,6 @@ 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 15a564a6..259b1a3f 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;
-}
-.LiveMemory-Debug:hover {
-  opacity: 1;
+  &:hover {
+    opacity: 1;
+  }
 }
 
 .LiveMemory-Debug button {
@@ -22,4 +22,7 @@
   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 01535af4..abbf1224 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;
-}
-.LiveMemory-Logo:hover {
-  opacity: .95;
+  &: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 dd5db0ce..bdbc685d 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;
-}
-.LiveMemory-Score button::after {
+
+  &::after {
     position: absolute;
     inset: 0;
     content: " ";
@@ -128,6 +128,7 @@
     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 713fabeb..402acbc6 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,16 +12,19 @@
   height: 2.25rem;
   border-radius: 12%;
   transition: transform 250ms linear;
-}
-[data-bs-theme="dark"] .LiveMemory-ThemeSwitch {
-  transform: rotateY(180deg);
-}
-.LiveMemory-ThemeSwitch[data-switch="dark"] {
+
+  [data-bs-theme="dark"] & {
     transform: rotateY(180deg);
-}
-.LiveMemory-ThemeSwitch[data-switch="light"] {
+  }
+
+  &[data-switch="dark"] {
+    transform: rotateY(180deg);
+  }
+
+  &[data-switch="light"] {
     transform: rotateY(0deg);
   }
+}
 
 .LiveMemory-ThemeSwitch-Mode {
   position: absolute;
@@ -45,18 +48,19 @@
   background: rgb(var(--color-background-rgb) / 100%);
 }
 
+
 .LiveMemory-ThemeSwitch-Dark {
   transform: rotateY(180deg);
-}
-.LiveMemory-ThemeSwitch-Dark:hover {
+  &:hover {
     background: var(--color-background);
     border: var(--color-border);
+  }
 }
 
 .LiveMemory-ThemeSwitch-Light {
   transform: rotateY(0deg);
-}
-.LiveMemory-ThemeSwitch-Light:hover {
+  &: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..b2ce3c77 100644
--- a/ux.symfony.com/importmap.php
+++ b/ux.symfony.com/importmap.php
@@ -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
deleted file mode 100644
index eb8ef3ec..00000000
--- a/ux.symfony.com/src/LiveMemory/Asset/CssCompiler.php
+++ /dev/null
@@ -1,57 +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 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/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 c3d5d52f..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-value="{{ 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/.github/workflows/test.yaml b/.github/workflows/test.yaml
index d9ae077e..f4d8b7f6 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -3,11 +3,9 @@ name: Symfony UX
 on:
     push:
         paths-ignore:
-            - 'src/*/doc/**'
             - 'ux.symfony.com/**'
     pull_request:
         paths-ignore:
-            - 'src/*/doc/**'
             - 'ux.symfony.com/**'
 
 jobs:
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/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 e32d4055..70493eb8 100644
--- a/src/LiveComponent/assets/test/controller/render.test.ts
+++ b/src/LiveComponent/assets/test/controller/render.test.ts
@@ -157,7 +157,7 @@ describe('LiveController rendering Tests', () => {
         `);
 
         test.expectsAjaxCall()
-            .expectUpdatedData({ name: 'Hello' });
+            .expectUpdatedData({ name: 'Hello' })
 
         const input = test.queryByDataModel('name') as HTMLInputElement;
         userEvent.type(input, 'Hello');
@@ -171,29 +171,6 @@ describe('LiveController rendering Tests', () => {
         expect(input.value).toBe('Hel!lo');
     });
 
-    it('uses the new value of an unmapped field that was NOT modified even if active', async () => {
-        const test = await createTest({ title: 'greetings' }, (data: any) => `
-            <div ${initComponent(data)}>
-                <!-- An unmapped field -->
-                <input value="${data.title}">
-
-                Title: "${data.title}"
-            </div>
-        `);
-
-        test.expectsAjaxCall()
-            .serverWillChangeProps((data: any) => {
-                // change the data on the server so the template renders differently
-                data.title = 'Hello';
-            });
-
-        const input = test.element.querySelector('input') as HTMLInputElement;
-        // focus the input, but don't change it
-        userEvent.type(input, '');
-        await test.component.render();
-        expect(input.value).toEqual('Hello');
-    });
-
     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 ac9404a7..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']['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/Component/DeferredComponentWithPlaceholder.php b/src/LiveComponent/tests/Fixtures/Component/DeferredComponentWithPlaceholder.php
deleted file mode 100644
index e7daba08..00000000
--- a/src/LiveComponent/tests/Fixtures/Component/DeferredComponentWithPlaceholder.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component;
-
-use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
-use Symfony\UX\LiveComponent\Attribute\LiveProp;
-use Symfony\UX\LiveComponent\DefaultActionTrait;
-
-#[AsLiveComponent('deferred_component_with_placeholder', method: 'get')]
-final class DeferredComponentWithPlaceholder
-{
-    use DefaultActionTrait;
-
-    #[LiveProp]
-    public int $rows = 6;
-}
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/Fixtures/templates/components/deferred_component_with_placeholder.html.twig b/src/LiveComponent/tests/Fixtures/templates/components/deferred_component_with_placeholder.html.twig
deleted file mode 100644
index fd546aa2..00000000
--- a/src/LiveComponent/tests/Fixtures/templates/components/deferred_component_with_placeholder.html.twig
+++ /dev/null
@@ -1,9 +0,0 @@
-<div {{ attributes }}>
-    {# for row in this.rows ... #}
-</div>
-
-{% macro placeholder(props) %}
-    {%- for i in 1..props.rows -%}
-        <span class="loading-row"></span>
-    {%- endfor -%}
-{% endmacro %}
diff --git a/src/LiveComponent/tests/Fixtures/templates/render_component_with_placeholder.html.twig b/src/LiveComponent/tests/Fixtures/templates/render_component_with_placeholder.html.twig
deleted file mode 100644
index c9b8d4ab..00000000
--- a/src/LiveComponent/tests/Fixtures/templates/render_component_with_placeholder.html.twig
+++ /dev/null
@@ -1,1 +0,0 @@
-<twig:deferred_component_with_placeholder rows="2" />
diff --git a/src/LiveComponent/tests/Fixtures/templates/render_deferred_component_with_placeholder.html.twig b/src/LiveComponent/tests/Fixtures/templates/render_deferred_component_with_placeholder.html.twig
deleted file mode 100644
index ce06cae0..00000000
--- a/src/LiveComponent/tests/Fixtures/templates/render_deferred_component_with_placeholder.html.twig
+++ /dev/null
@@ -1,1 +0,0 @@
-<twig:deferred_component_with_placeholder rows="2" defer />
diff --git a/src/LiveComponent/tests/Functional/EventListener/DeferLiveComponentSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/DeferLiveComponentSubscriberTest.php
index 420fce3b..d243ccba 100644
--- a/src/LiveComponent/tests/Functional/EventListener/DeferLiveComponentSubscriberTest.php
+++ b/src/LiveComponent/tests/Functional/EventListener/DeferLiveComponentSubscriberTest.php
@@ -77,27 +77,6 @@ final class DeferLiveComponentSubscriberTest extends KernelTestCase
         $this->assertStringContainsString('Long awaited data', $div->html());
     }
 
-    public function testItIncludesComponentTemplateBlockAsPlaceholder(): void
-    {
-        $div = $this->browser()
-            ->visit('/render-template/render_deferred_component_with_placeholder')
-            ->assertSuccessful()
-            ->crawler()
-            ->filter('div');
-
-        $this->assertSame('<span class="loading-row"></span><span class="loading-row"></span>', trim($div->html()));
-    }
-
-    public function testItDoesNotIncludesPlaceholderWhenRendered(): void
-    {
-        $div = $this->browser()
-            ->visit('/render-template/render_component_with_placeholder')
-            ->assertSuccessful()
-            ->crawler();
-
-        $this->assertStringNotContainsString('<span class="loading-row">', $div->html());
-    }
-
     public function testItAllowsToSetCustomLoadingHtmlTag(): void
     {
         $crawler = $this->browser()
diff --git a/src/LiveComponent/tests/Functional/EventListener/InterceptChildComponentRenderSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/InterceptChildComponentRenderSubscriberTest.php
index 19cd1740..3dfd77bb 100644
--- a/src/LiveComponent/tests/Functional/EventListener/InterceptChildComponentRenderSubscriberTest.php
+++ b/src/LiveComponent/tests/Functional/EventListener/InterceptChildComponentRenderSubscriberTest.php
@@ -128,7 +128,7 @@ final class InterceptChildComponentRenderSubscriberTest extends KernelTestCase
             ->assertSuccessful()
             ->assertHtml()
             ->assertElementCount('ul li', 3)
-            // check for the id we expect based on the key
+            // check for the live-id we expect based on the key
             ->assertContains('id="live-521026374-the-key0"')
             ->assertNotContains('key="the-key0"')
             ->visit($urlWithChangedFingerprints)
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/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>