From 39e1f35c18149a0a3f10dd8ae21fc3384eac3057 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Fri, 24 Jan 2025 16:10:34 +0100
Subject: [PATCH 01/45] Manage complex form styling.

---
 css/drupal.form.css             | 15 +++++++
 css/drupal.tabledrag.css        | 29 ++++++++++++
 css/drupal.vertical-tabs.css    | 12 +++++
 dsfr4drupal.info.yml            |  6 +++
 dsfr4drupal.libraries.yml       | 17 +++++++
 js/drupal.tabledrag.js          | 78 +++++++++++++++++++++++++++++++++
 templates/layout/page.html.twig |  2 +-
 7 files changed, 158 insertions(+), 1 deletion(-)
 create mode 100644 css/drupal.form.css
 create mode 100644 css/drupal.tabledrag.css
 create mode 100644 css/drupal.vertical-tabs.css
 create mode 100644 js/drupal.tabledrag.js

diff --git a/css/drupal.form.css b/css/drupal.form.css
new file mode 100644
index 0000000..d9c96a3
--- /dev/null
+++ b/css/drupal.form.css
@@ -0,0 +1,15 @@
+
+
+:root {
+  --form-spacing: 1.5rem;
+  --form-item-spacing: var(--form-spacing);
+}
+
+main[role="main"] > .fr-container > form,
+main[role="main"] > .fr-container--fluid > form {
+  margin-block: var(--form-spacing);
+}
+
+.form-item {
+  margin-block: var(--form-item-spacing);
+}
diff --git a/css/drupal.tabledrag.css b/css/drupal.tabledrag.css
new file mode 100644
index 0000000..1d1e775
--- /dev/null
+++ b/css/drupal.tabledrag.css
@@ -0,0 +1,29 @@
+/**
+ * @file
+ * Manage tabledrag styling.
+ */
+
+a.tabledrag-handle[href] {
+  background-image: none;
+}
+
+.draggable a.tabledrag-handle {
+  margin: 0;
+}
+
+a.tabledrag-handle .handle {
+  margin: 0;
+  padding: 0;
+  min-width: 16px;
+  min-height: 16px;
+  height: 100%;
+  background-position: center center;
+}
+
+.field-multiple-drag {
+  max-width: fit-content;
+}
+
+.tabledrag-toggle-weight {
+  font-size: .75em;
+}
diff --git a/css/drupal.vertical-tabs.css b/css/drupal.vertical-tabs.css
new file mode 100644
index 0000000..1d03a4e
--- /dev/null
+++ b/css/drupal.vertical-tabs.css
@@ -0,0 +1,12 @@
+/**
+ * @file
+ * Manage vertical tabs styling.
+ */
+
+.vertical-tabs__menu [href] {
+  background-image: none;
+}
+
+.vertical-tabs__pane {
+  padding: 1.5rem;
+}
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index 17068cb..237e4af 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -36,8 +36,14 @@ libraries:
   - dsfr4drupal/utility
 
 libraries-extend:
+  core/drupal.form:
+    - dsfr4drupal/drupal.form
   core/drupal.message:
     - dsfr4drupal/drupal.message
+  core/drupal.tabledrag:
+    - dsfr4drupal/drupal.tabledrag
+  core/drupal.vertical-tabs:
+    - dsfr4drupal/drupal.vertical-tabs
   navigation/navigation.layout:
     - dsfr4drupal/navigation.layout
   node/drupal.node.preview:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index 3ccc4c0..0857cec 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -545,6 +545,11 @@ core:
       production: true
       verbose: true
 
+drupal.form:
+  css:
+    theme:
+      css/drupal.form.css: {}
+
 drupal.message:
   js:
     js/messages.js: {}
@@ -556,6 +561,18 @@ drupal.node.preview:
     theme:
       css/drupal.node.preview.css: {}
 
+drupal.tabledrag:
+  css:
+    theme:
+      css/drupal.tabledrag.css: {}
+  js:
+    js/drupal.tabledrag.js: {}
+
+drupal.vertical-tabs:
+  css:
+    theme:
+      css/drupal.vertical-tabs.css: {}
+
 #legacy:
 #  js:
 #    /libraries/dsfr/dist/legacy/legacy.nomodule.min.js:
diff --git a/js/drupal.tabledrag.js b/js/drupal.tabledrag.js
new file mode 100644
index 0000000..f363909
--- /dev/null
+++ b/js/drupal.tabledrag.js
@@ -0,0 +1,78 @@
+/**
+ * @file
+ * Provide dragging capabilities to admin uis.
+ */
+
+/**
+ * Triggers when weights columns are toggled.
+ *
+ * @event columnschange
+ */
+
+(function ($, Drupal, drupalSettings) {
+  /**
+   * Store the state of weight columns display for all tables.
+   *
+   * Default value is to hide weight columns.
+   */
+  let showWeight = JSON.parse(
+    localStorage.getItem("Drupal.tableDrag.showWeight"),
+  );
+
+  // React to localStorage event showing or hiding weight columns.
+  $(window).on(
+    "storage",
+    function (e) {
+      // Only react to "Drupal.tableDrag.showWeight" value change.
+      if (e.originalEvent.key === "Drupal.tableDrag.showWeight") {
+        // This was changed in another window, get the new value for this
+        // window.
+        showWeight = JSON.parse(e.originalEvent.newValue);
+        this.displayColumns(showWeight);
+      }
+    }.bind(this),
+  );
+
+  /**
+   * Hide the columns containing weight/parent form elements.
+   *
+   * Undo showColumns().
+   */
+  Drupal.tableDrag.prototype.hideColumns = function () {
+    const $tables = $(once.filter('tabledrag', 'table'));
+    // Hide weight/parent cells and headers.
+    $tables.find('.tabledrag-hide').each(function () {
+      this.style.display = 'none';
+    });
+    // Show TableDrag handles.
+    $tables.find('.tabledrag-handle').each(function () {
+      this.style.display = '';
+    });
+    // Reduce the colspan of any effected multi-span columns.
+    $tables.find('.tabledrag-has-colspan').each(function () {
+      this.colSpan -= 1;
+    });
+  };
+
+  /**
+   * Show the columns containing weight/parent form elements.
+   *
+   * Undo hideColumns().
+   */
+  Drupal.tableDrag.prototype.showColumns = function () {
+    const $tables = $(once.filter('tabledrag', 'table'));
+    // Show weight/parent cells and headers.
+    $tables.find('.tabledrag-hide').each(function () {
+      this.style.display = '';
+    });
+    // Hide TableDrag handles.
+    $tables.find('.tabledrag-handle').each(function () {
+      this.style.display = 'none';
+    });
+    // Increase the colspan for any columns where it was previously reduced.
+    $tables.find('.tabledrag-has-colspan').each(function () {
+      this.colSpan += 1;
+    });
+  };
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/templates/layout/page.html.twig b/templates/layout/page.html.twig
index 3d79ad9..0433eea 100644
--- a/templates/layout/page.html.twig
+++ b/templates/layout/page.html.twig
@@ -11,7 +11,7 @@
 <div class="{{ container_class|default('fr-container') }}">
   {{ page.breadcrumb }}
 </div>
-<main id="main" role="main" class="{{ container_class|default('fr-container') }}" tabindex="-1">
+<main id="main" role="main" tabindex="-1">
   <div class="{{ container_class|default('fr-container') }}">
     {{ page.content }}
   </div>
-- 
GitLab


From 3518edfdd9febbf2c23dee63b6ef40d8e2480518 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sat, 25 Jan 2025 12:06:08 +0100
Subject: [PATCH 02/45] Manage tabledrag toggle weight button styling.

---
 css/drupal.form.css                           |  4 +-
 css/drupal.tabledrag.css                      |  4 +
 js/drupal.tabledrag.js                        | 90 +++++++++----------
 .../form/field-multiple-value-form.html.twig  |  2 +-
 4 files changed, 48 insertions(+), 52 deletions(-)

diff --git a/css/drupal.form.css b/css/drupal.form.css
index d9c96a3..59f76c9 100644
--- a/css/drupal.form.css
+++ b/css/drupal.form.css
@@ -1,4 +1,6 @@
-
+/**
+ * Mange from styling with DSFR.
+ */
 
 :root {
   --form-spacing: 1.5rem;
diff --git a/css/drupal.tabledrag.css b/css/drupal.tabledrag.css
index 1d1e775..55733fc 100644
--- a/css/drupal.tabledrag.css
+++ b/css/drupal.tabledrag.css
@@ -27,3 +27,7 @@ a.tabledrag-handle .handle {
 .tabledrag-toggle-weight {
   font-size: .75em;
 }
+
+.tabledrag-toggle-weight-wrapper + .fr-table {
+  margin-top: 0;
+}
diff --git a/js/drupal.tabledrag.js b/js/drupal.tabledrag.js
index f363909..8d38816 100644
--- a/js/drupal.tabledrag.js
+++ b/js/drupal.tabledrag.js
@@ -19,60 +19,50 @@
     localStorage.getItem("Drupal.tableDrag.showWeight"),
   );
 
-  // React to localStorage event showing or hiding weight columns.
-  $(window).on(
-    "storage",
-    function (e) {
-      // Only react to "Drupal.tableDrag.showWeight" value change.
-      if (e.originalEvent.key === "Drupal.tableDrag.showWeight") {
-        // This was changed in another window, get the new value for this
-        // window.
-        showWeight = JSON.parse(e.originalEvent.newValue);
-        this.displayColumns(showWeight);
-      }
-    }.bind(this),
-  );
+  const initColumnsOriginal = Drupal.tableDrag.prototype.initColumns;
 
   /**
-   * Hide the columns containing weight/parent form elements.
-   *
-   * Undo showColumns().
+   * @inheritDoc
    */
-  Drupal.tableDrag.prototype.hideColumns = function () {
-    const $tables = $(once.filter('tabledrag', 'table'));
-    // Hide weight/parent cells and headers.
-    $tables.find('.tabledrag-hide').each(function () {
-      this.style.display = 'none';
-    });
-    // Show TableDrag handles.
-    $tables.find('.tabledrag-handle').each(function () {
-      this.style.display = '';
-    });
-    // Reduce the colspan of any effected multi-span columns.
-    $tables.find('.tabledrag-has-colspan').each(function () {
-      this.colSpan -= 1;
-    });
-  };
+  Drupal.tableDrag.prototype.initColumns = function () {
+    const $tableWrapper = this.$table.parents(".fr-table");
+    const $toggleWeightWrapper = this.$table.prev();
 
-  /**
-   * Show the columns containing weight/parent form elements.
-   *
-   * Undo hideColumns().
-   */
-  Drupal.tableDrag.prototype.showColumns = function () {
-    const $tables = $(once.filter('tabledrag', 'table'));
-    // Show weight/parent cells and headers.
-    $tables.find('.tabledrag-hide').each(function () {
-      this.style.display = '';
-    });
-    // Hide TableDrag handles.
-    $tables.find('.tabledrag-handle').each(function () {
-      this.style.display = 'none';
-    });
-    // Increase the colspan for any columns where it was previously reduced.
-    $tables.find('.tabledrag-has-colspan').each(function () {
-      this.colSpan += 1;
-    });
+    // Move toggle weight wrapper before DSFR table wrapper.
+    $tableWrapper.before($toggleWeightWrapper);
+
+    // Call original "initColumns" method.
+    initColumnsOriginal.call(this);
   };
 
+  $.extend(
+    Drupal.theme,
+    /** @lends Drupal.theme */ {
+      /**
+       * Constructs contents of the toggle weight button.
+       *
+       * @param {boolean} show
+       *   If the table weights are currently displayed.
+       *
+       * @return {string}
+       *  HTML markup for the weight toggle button content.
+       */
+      toggleButtonContent: (show) => {
+        const classes = [
+          'tabledrag-toggle-weight',
+          'fr-icon--sm',
+        ];
+        let text = '';
+        if (show) {
+          classes.push('fr-icon-eye-off-line');
+          text = Drupal.t('Hide row weights');
+        } else {
+          classes.push('fr-icon-eye-line');
+          text = Drupal.t('Show row weights');
+        }
+        return `<span class="${classes.join(' ')}" aria-hidden="true"></span>&nbsp;${text}`;
+      },
+    },
+  );
+
 })(jQuery, Drupal, drupalSettings);
diff --git a/templates/form/field-multiple-value-form.html.twig b/templates/form/field-multiple-value-form.html.twig
index 46964d3..6654c13 100644
--- a/templates/form/field-multiple-value-form.html.twig
+++ b/templates/form/field-multiple-value-form.html.twig
@@ -1,7 +1,7 @@
 {% if multiple %}
   {% set classes = [
     'js-form-item',
-    'form-item'
+    'form-item',
   ] %}
 
   <div{{ attributes.addClass(classes) }}>
-- 
GitLab


From efa84aae5202d821053e1735f74c81b0ab5b40e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sat, 25 Jan 2025 12:09:04 +0100
Subject: [PATCH 03/45] Fix input width into vertical tabs pane.

---
 css/drupal.vertical-tabs.css | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/css/drupal.vertical-tabs.css b/css/drupal.vertical-tabs.css
index 1d03a4e..be39efc 100644
--- a/css/drupal.vertical-tabs.css
+++ b/css/drupal.vertical-tabs.css
@@ -10,3 +10,7 @@
 .vertical-tabs__pane {
   padding: 1.5rem;
 }
+
+.vertical-tabs__pane .fr-input {
+  box-sizing: border-box;
+}
-- 
GitLab


From 5507a9bfaa66bd685625feae7509c0dc643f32c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sat, 25 Jan 2025 19:01:18 +0100
Subject: [PATCH 04/45] Manage media library styles.

---
 components/card/card.component.yml            |  3 ++
 components/card/card.twig                     |  3 +-
 css/component/checkbox.css                    | 18 +++++++
 css/component/form.css                        | 44 +++++++++++++++++
 .../node-preview.css}                         |  0
 .../tabledrag.css}                            |  0
 css/{ => component}/toolbar.css               |  0
 .../vertical-tabs.css}                        |  0
 css/component/views-exposed-form.css          | 41 ++++++++++++++++
 css/drupal.form.css                           | 17 -------
 css/{ => theme}/card.css                      |  0
 css/{ => theme}/display.button.css            |  0
 css/theme/media-library.css                   | 33 +++++++++++++
 css/{ => theme}/pager.css                     |  0
 css/{ => theme}/tile.css                      |  0
 css/{ => theme}/tooltip.css                   |  0
 dsfr4drupal.info.yml                          |  6 +++
 dsfr4drupal.libraries.yml                     | 49 ++++++++++++-------
 includes/field.theme                          | 12 +++++
 includes/form.theme                           | 41 ++++++++++++++++
 includes/media.theme                          | 18 +++++++
 includes/views.theme                          | 35 +++++++++++++
 js/{drupal.tabledrag.js => tabledrag.js}      |  0
 .../field/field--media--thumbnail.html.twig   |  2 +
 .../field/field--node--created.html.twig      |  2 +-
 templates/field/field--node--title.html.twig  |  2 +-
 .../form/form-element--checkbox.html.twig     | 26 ++++++++++
 templates/form/form-element--radio.html.twig  |  1 +
 templates/form/form-element.html.twig         |  2 +-
 .../media/media--media-library.html.twig      | 20 ++++++++
 .../views/views-view--media-library.html.twig | 45 +++++++++++++++++
 ...-view-unformatted--media-library.html.twig | 15 ++++++
 32 files changed, 397 insertions(+), 38 deletions(-)
 create mode 100644 css/component/checkbox.css
 create mode 100644 css/component/form.css
 rename css/{drupal.node.preview.css => component/node-preview.css} (100%)
 rename css/{drupal.tabledrag.css => component/tabledrag.css} (100%)
 rename css/{ => component}/toolbar.css (100%)
 rename css/{drupal.vertical-tabs.css => component/vertical-tabs.css} (100%)
 create mode 100644 css/component/views-exposed-form.css
 delete mode 100644 css/drupal.form.css
 rename css/{ => theme}/card.css (100%)
 rename css/{ => theme}/display.button.css (100%)
 create mode 100644 css/theme/media-library.css
 rename css/{ => theme}/pager.css (100%)
 rename css/{ => theme}/tile.css (100%)
 rename css/{ => theme}/tooltip.css (100%)
 create mode 100644 includes/media.theme
 create mode 100644 includes/views.theme
 rename js/{drupal.tabledrag.js => tabledrag.js} (100%)
 create mode 100644 templates/field/field--media--thumbnail.html.twig
 create mode 100644 templates/form/form-element--checkbox.html.twig
 create mode 100644 templates/form/form-element--radio.html.twig
 create mode 100644 templates/media/media--media-library.html.twig
 create mode 100644 templates/views/views-view--media-library.html.twig
 create mode 100644 templates/views/views-view-unformatted--media-library.html.twig

diff --git a/components/card/card.component.yml b/components/card/card.component.yml
index 6b9c967..cc6b0da 100644
--- a/components/card/card.component.yml
+++ b/components/card/card.component.yml
@@ -72,6 +72,9 @@ props:
     title:
       type: string
       title: Title
+    title_attributes:
+      type: Drupal\Core\Template\Attribute
+      title: Title attributes
     title_tag:
       type: string
       title: Title HTML tag
diff --git a/components/card/card.twig b/components/card/card.twig
index 1f95bf3..1b32a00 100644
--- a/components/card/card.twig
+++ b/components/card/card.twig
@@ -1,6 +1,7 @@
 {% set attributes = attributes|default(create_attribute()) %}
 {% set title_tag = title_tag|default('h3') %}
 {% set link_attributes = link_attributes|default(create_attribute()) %}
+{% set title_attributes = title_attributes|default(create_attribute()) %}
 
 {% if variant %}
   {% set attributes = attributes.addClass('fr-card--' ~ variant) %}
@@ -32,7 +33,7 @@
 <div{{ attributes.addClass('fr-card') }}>
   <div class="fr-card__body">
     <div class="fr-card__content">
-      <{{ title_tag }} class="fr-card__title">
+      <{{ title_tag }}{{ title_attributes.addClass('fr-card__title') }}>
         {% if use_button %}
           <buton{{ link_attributes }}>{{ title }}</buton>
         {% elseif url %}
diff --git a/css/component/checkbox.css b/css/component/checkbox.css
new file mode 100644
index 0000000..be72fa3
--- /dev/null
+++ b/css/component/checkbox.css
@@ -0,0 +1,18 @@
+/**
+ * @file
+ * Manage checkbox styles.
+ */
+
+/* Fix checkbox rendering when label is visually hidden */
+.fr-checkbox-group input[type="checkbox"] + label.visually-hidden {
+  width: 1.5rem;
+  height: 1.5rem;
+  margin-left: .5rem;
+  margin-top: .5rem;
+  padding-left: 1.5rem;
+  clip: auto;
+  z-index: 1;
+}
+.fr-checkbox-group input[type="checkbox"] + label.visually-hidden::before {
+  left: 0;
+}
diff --git a/css/component/form.css b/css/component/form.css
new file mode 100644
index 0000000..51e959b
--- /dev/null
+++ b/css/component/form.css
@@ -0,0 +1,44 @@
+/**
+ * @file
+ * Manage form styles.
+ */
+
+:root {
+  --form-spacing: 1.5rem;
+  --form-spacing-s: 1rem;
+  --form-spacing-xs: .5rem;
+  --form-item-spacing: var(--form-spacing);
+
+  /* Label font-size * line-height */
+  --form-label-line-height: calc(1rem * 1.5rem);
+  /* Spacing between label and input. */
+  --form-label-input-spacing: .5rem;
+}
+
+main[role="main"] > .fr-container > form,
+main[role="main"] > .fr-container--fluid > form {
+  margin-block: var(--form-spacing);
+}
+
+.form-actions,
+.form-item {
+  margin-block: var(--form-item-spacing);
+}
+
+.views-exposed-form {
+  display: flex;
+  flex-wrap: wrap;
+  margin-block: var(--form-spacing);
+  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing) var(--form-spacing);
+  border: 1px solid var(--border-plain-grey);
+}
+
+.fr-upload-group {
+  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing) var(--form-spacing);
+  border: 1px solid var(--border-plain-grey);
+}
+
+.fr-upload-group > label {
+  font-weight: 700;
+}
+
diff --git a/css/drupal.node.preview.css b/css/component/node-preview.css
similarity index 100%
rename from css/drupal.node.preview.css
rename to css/component/node-preview.css
diff --git a/css/drupal.tabledrag.css b/css/component/tabledrag.css
similarity index 100%
rename from css/drupal.tabledrag.css
rename to css/component/tabledrag.css
diff --git a/css/toolbar.css b/css/component/toolbar.css
similarity index 100%
rename from css/toolbar.css
rename to css/component/toolbar.css
diff --git a/css/drupal.vertical-tabs.css b/css/component/vertical-tabs.css
similarity index 100%
rename from css/drupal.vertical-tabs.css
rename to css/component/vertical-tabs.css
diff --git a/css/component/views-exposed-form.css b/css/component/views-exposed-form.css
new file mode 100644
index 0000000..d465cc8
--- /dev/null
+++ b/css/component/views-exposed-form.css
@@ -0,0 +1,41 @@
+/**
+ * @file
+ * Manage styles for views exposed form.
+ */
+
+/**
+ * Use flexbox and some margin resets to make the fields + actions go inline.
+ */
+.views-exposed-form {
+  display: flex;
+  flex-wrap: wrap;
+  margin-block: var(--form-spacing-s);
+  margin-inline: var(--form-spacing-s);
+  padding: var(--form-spacing-xs) var(--form-spacing-s) var(--form-spacing-s);
+}
+
+.views-exposed-form--preview.views-exposed-form--preview {
+  margin-top: 0;
+}
+
+.views-exposed-form__item {
+  max-width: 100%;
+  margin-block: var(--form-spacing-s) 0;
+  margin-inline: 0 var(--form-spacing-xs);
+}
+
+.views-exposed-form .form-item--no-label,
+.views-exposed-form__item.views-exposed-form__item.views-exposed-form__item--actions {
+  margin-block: var(--form-spacing-s) 0;
+  align-self: flex-end;
+}
+
+.views-exposed-form .form-item--no-label,
+.views-exposed-form__item.views-exposed-form__item--actions {
+  margin-top: calc(var(--form-label-line-height) + var(--form-label-input-spacing));
+}
+
+.views-exposed-form .fr-input-group:not(:last-child),
+.views-exposed-form .fr-select-group:not(:last-child) {
+  margin-bottom: 0;
+}
diff --git a/css/drupal.form.css b/css/drupal.form.css
deleted file mode 100644
index 59f76c9..0000000
--- a/css/drupal.form.css
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Mange from styling with DSFR.
- */
-
-:root {
-  --form-spacing: 1.5rem;
-  --form-item-spacing: var(--form-spacing);
-}
-
-main[role="main"] > .fr-container > form,
-main[role="main"] > .fr-container--fluid > form {
-  margin-block: var(--form-spacing);
-}
-
-.form-item {
-  margin-block: var(--form-item-spacing);
-}
diff --git a/css/card.css b/css/theme/card.css
similarity index 100%
rename from css/card.css
rename to css/theme/card.css
diff --git a/css/display.button.css b/css/theme/display.button.css
similarity index 100%
rename from css/display.button.css
rename to css/theme/display.button.css
diff --git a/css/theme/media-library.css b/css/theme/media-library.css
new file mode 100644
index 0000000..6acf699
--- /dev/null
+++ b/css/theme/media-library.css
@@ -0,0 +1,33 @@
+/**
+ * @file
+ * Manage media library styles.
+ */
+
+.media-library-content {
+  padding: 1em;
+}
+
+.js-media-library-item {
+  margin-block: var(--form-item-spacing) !important;
+}
+
+.js-media-library-add-form-added-media {
+  --ul-type: none;
+}
+
+.js-media-library-add-form-added-media li {
+  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing) var(--form-spacing);
+  border: 1px solid var(--border-plain-grey);
+}
+
+.js-media-library-add-form-added-media img {
+  display: block;
+  margin: 0 auto;
+  max-width: 100%;
+  height: auto;
+}
+
+.js-media-library-add-form-added-media .form-item-name label {
+  display: inline-block;
+  font-weight: 700;
+}
diff --git a/css/pager.css b/css/theme/pager.css
similarity index 100%
rename from css/pager.css
rename to css/theme/pager.css
diff --git a/css/tile.css b/css/theme/tile.css
similarity index 100%
rename from css/tile.css
rename to css/theme/tile.css
diff --git a/css/tooltip.css b/css/theme/tooltip.css
similarity index 100%
rename from css/tooltip.css
rename to css/theme/tooltip.css
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index 237e4af..30ba970 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -44,6 +44,10 @@ libraries-extend:
     - dsfr4drupal/drupal.tabledrag
   core/drupal.vertical-tabs:
     - dsfr4drupal/drupal.vertical-tabs
+  media_library/view:
+    - dsfr4drupal/media_library.theme
+  media_library/widget:
+    - dsfr4drupal/media_library.theme
   navigation/navigation.layout:
     - dsfr4drupal/navigation.layout
   node/drupal.node.preview:
@@ -54,6 +58,8 @@ libraries-extend:
     - dsfr4drupal/tarteaucitron
   toolbar/toolbar:
     - dsfr4drupal/toolbar
+  views/views.module:
+    - dsfr4drupal/views
 
 libraries-override:
   tacjs/tarteaucitron.js:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index 0857cec..bdaf068 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -62,7 +62,7 @@ component.card:
     component:
       /libraries/dsfr/dist/component/card/card.min.css: { minified: true }
     theme:
-      css/card.css: {}
+      css/theme/card.css: {}
   dependencies:
     - dsfr4drupal/component.button
     - dsfr4drupal/core
@@ -71,6 +71,7 @@ component.checkbox:
   css:
     component:
       /libraries/dsfr/dist/component/checkbox/checkbox.min.css: { minified: true }
+      css/component/checkbox.css: {}
   dependencies:
     - dsfr4drupal/component.form
     - dsfr4drupal/core
@@ -111,7 +112,7 @@ component.display:
 component.display.button:
   css:
     theme:
-      css/display.button.css: {}
+      css/theme/display.button.css: {}
 
 component.connect:
   css:
@@ -242,7 +243,7 @@ component.pagination:
     component:
       /libraries/dsfr/dist/component/pagination/pagination.min.css: { minified: true }
     theme:
-      css/pager.css: {}
+      css/theme/pager.css: {}
   dependencies:
     - dsfr4drupal/core
 
@@ -425,7 +426,7 @@ component.tile:
     component:
       /libraries/dsfr/dist/component/tile/tile.min.css: { minified: true }
     theme:
-      css/tile.css: {}
+      css/theme/tile.css: {}
   dependencies:
     - dsfr4drupal/core
 
@@ -452,7 +453,7 @@ component.tooltip:
     component:
       /libraries/dsfr/dist/component/tooltip/tooltip.min.css: { minified: true }
     theme:
-      css/tooltip.css: {}
+      css/theme/tooltip.css: {}
   js:
     /libraries/dsfr/dist/component/tooltip/tooltip.module.min.js:
       minified: true
@@ -547,8 +548,8 @@ core:
 
 drupal.form:
   css:
-    theme:
-      css/drupal.form.css: {}
+    component:
+      css/component/form.css: {}
 
 drupal.message:
   js:
@@ -558,20 +559,20 @@ drupal.message:
 
 drupal.node.preview:
   css:
-    theme:
-      css/drupal.node.preview.css: {}
+    component:
+      css/component/node-preview.css: {}
 
 drupal.tabledrag:
   css:
-    theme:
-      css/drupal.tabledrag.css: {}
+    component:
+      css/component/tabledrag.css: {}
   js:
-    js/drupal.tabledrag.js: {}
+    js/tabledrag.js: {}
 
 drupal.vertical-tabs:
   css:
-    theme:
-      css/drupal.vertical-tabs.css: {}
+    component:
+      css/component/vertical-tabs.css: {}
 
 #legacy:
 #  js:
@@ -582,10 +583,17 @@ drupal.vertical-tabs:
 #      # Move DSFR modules to first load to improve Javascript file aggregation.
 #      weight: -50
 
+media_library.theme:
+  css:
+    theme:
+      css/theme/media-library.css: {}
+  dependencies:
+    - dsfr4drupal/core
+
 navigation.layout:
   css:
     theme:
-      css/navigation.header.css: {}
+      css/theme/navigation.header.css: {}
 
 scheme:
   css:
@@ -608,8 +616,8 @@ tarteaucitron:
 
 toolbar:
   css:
-    theme:
-      css/toolbar.css: {}
+    component:
+      css/component/toolbar.css: {}
 
 utility:
   css:
@@ -622,3 +630,10 @@ utility.icons:
   css:
     base:
       /libraries/dsfr/dist/utility/icons/icons.min.css: { minified: true }
+
+views:
+  css:
+    component:
+      css/component/views-exposed-form.css: {}
+  dependencies:
+    - dsfr4drupal/drupal.form
diff --git a/includes/field.theme b/includes/field.theme
index a9988cd..f1dec96 100644
--- a/includes/field.theme
+++ b/includes/field.theme
@@ -32,3 +32,15 @@ function dsfr4drupal_preprocess_field(array &$variables): void {
     }
   }
 }
+
+/**
+ * Implements hook_preprocess_HOOK() for "field__media__thumbnail".
+ */
+function dsfr4drupal_preprocess_field__media__thumbnail(array &$variables): void {
+  $items = &$variables['items'];
+
+  // Add "fr-responsive-img" by default.
+  foreach ($items as &$item) {
+    $item['content']['#item_attributes']['class'][] = 'fr-responsive-img';
+  }
+}
diff --git a/includes/form.theme b/includes/form.theme
index b2a2ebf..d639458 100644
--- a/includes/form.theme
+++ b/includes/form.theme
@@ -9,6 +9,7 @@ declare(strict_types=1);
 
 use Drupal\Component\Utility\Crypt;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
 
 /**
  * Implements hook_form_FORM_ID_alter() for "node_preview_form_select".
@@ -58,6 +59,29 @@ function dsfr4drupal_preprocess_form(array &$variables): void {
   }
 }
 
+/**
+ * Implements hook_preprocess_HOOK() for "views_exposed_form".
+ */
+function dsfr4drupal_preprocess_views_exposed_form(&$variables) {
+  $form = &$variables['form'];
+
+  // Add BEM classes for items in the form.
+  // Sorted keys.
+  foreach (Element::children($form, TRUE) as $child_key) {
+    if (!empty($form[$child_key]['#type'])) {
+      if ($form[$child_key]['#type'] === 'actions') {
+        // We need the key of the element that precedes the actions element.
+        $form[$child_key]['#attributes']['class'][] = 'views-exposed-form__item';
+        $form[$child_key]['#attributes']['class'][] = 'views-exposed-form__item--actions';
+      }
+
+      if (!in_array($form[$child_key]['#type'], ['hidden', 'actions'])) {
+        $form[$child_key]['#wrapper_attributes']['class'][] = 'views-exposed-form__item';
+      }
+    }
+  }
+}
+
 /**
  * Implements hook_preprocess_HOOK() for "webform".
  */
@@ -253,6 +277,23 @@ function dsfr4drupal_preprocess_textarea(array &$variables): void {
   }
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK_alter() for "form_elemtn".
+ */
+function dsfr4drupal_theme_suggestions_form_element_alter(array &$suggestions, array $variables): void {
+  if (empty($suggestions)) {
+    $suggestions[] = 'form_element';
+  }
+
+  // Set suggestions by form element type.
+  $new_suggestions = [];
+  foreach ($suggestions as $suggestion) {
+    $new_suggestions[] = $suggestion;
+    $new_suggestions[] = $suggestion . '__' . $variables['element']['#type'];
+  }
+  $suggestions = $new_suggestions;
+}
+
 /**
  * Implements hook_theme_suggestions_HOOK_alter() for "input".
  */
diff --git a/includes/media.theme b/includes/media.theme
new file mode 100644
index 0000000..d98dfcb
--- /dev/null
+++ b/includes/media.theme
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @file
+ * Functions to support media theming in the "DSFR for Drupal" theme.
+ */
+
+declare(strict_types=1);
+
+/**
+ * Implements hook_preprocss_hook() for "media__media_library".
+ */
+function dsfr4drupal_preprocess_media__media_library(array &$variables): void {
+  /** @var \Drupal\media\MediaInterface $media */
+  $media = $variables['media'];
+
+
+}
diff --git a/includes/views.theme b/includes/views.theme
new file mode 100644
index 0000000..176e8f4
--- /dev/null
+++ b/includes/views.theme
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Functions to support views theming in the "DSFR for Drupal" theme.
+ */
+
+declare(strict_types=1);
+
+/**
+ * Implements hook_preprocss_hook() for "views_view__media_library".
+ */
+function dsfr4drupal_preprocess_views_view__media_library(array &$variables): void {
+  if (empty($variables['header'])) {
+    return;
+  }
+
+  foreach ($variables['header'] as &$header) {
+    $options = &$header['#options'];
+
+    // Add tab component attributes.
+    $options['attributes']['aria-selected'] = 'false';
+    $options['attributes']['class'][] = 'fr-tabs__tab';
+    $options['attributes']['role'] = 'tab';
+
+    // Set tab active.
+    if ($options['view']->current_display === $options['target_display_id']) {
+      $options['attributes']['aria-selected'] = 'true';
+      $options['attributes']['class'][] = 'fr-tabs__tab--selected';
+    }
+  }
+
+  $variables['#attached']['library'][] = 'dsfr4drupal/component.tab';
+  $variables['#attached']['library'][] = 'dsfr4drupal/component.link';
+}
diff --git a/js/drupal.tabledrag.js b/js/tabledrag.js
similarity index 100%
rename from js/drupal.tabledrag.js
rename to js/tabledrag.js
diff --git a/templates/field/field--media--thumbnail.html.twig b/templates/field/field--media--thumbnail.html.twig
new file mode 100644
index 0000000..df508d3
--- /dev/null
+++ b/templates/field/field--media--thumbnail.html.twig
@@ -0,0 +1,2 @@
+{# Include default "field" template without alter it to benefits of preprocessing. #}
+{% include 'field.html.twig' %}
diff --git a/templates/field/field--node--created.html.twig b/templates/field/field--node--created.html.twig
index 8207766..2e5679c 100644
--- a/templates/field/field--node--created.html.twig
+++ b/templates/field/field--node--created.html.twig
@@ -1,5 +1,5 @@
 {% if not is_inline %}
-  {% include "field.html.twig" %}
+  {% include 'field.html.twig' %}
 {% else %}
   {%- if attributes is not empty -%}
     <span{{ attributes }}>
diff --git a/templates/field/field--node--title.html.twig b/templates/field/field--node--title.html.twig
index 8207766..2e5679c 100644
--- a/templates/field/field--node--title.html.twig
+++ b/templates/field/field--node--title.html.twig
@@ -1,5 +1,5 @@
 {% if not is_inline %}
-  {% include "field.html.twig" %}
+  {% include 'field.html.twig' %}
 {% else %}
   {%- if attributes is not empty -%}
     <span{{ attributes }}>
diff --git a/templates/form/form-element--checkbox.html.twig b/templates/form/form-element--checkbox.html.twig
new file mode 100644
index 0000000..ebc4eb6
--- /dev/null
+++ b/templates/form/form-element--checkbox.html.twig
@@ -0,0 +1,26 @@
+{% set classes = [
+  'js-form-item',
+  'form-item',
+  'js-form-type-' ~ type|clean_class,
+  'form-item-' ~ name|clean_class,
+  'js-form-item-' ~ name|clean_class,
+  title_display not in ['after', 'before'] ? 'form-item--no-label',
+  disabled == 'disabled' ? 'form-disabled',
+] %}
+
+<div{{ attributes.addClass(classes) }}>
+  {% if prefix is not empty %}
+    <span class="field-prefix">{{ prefix }}</span>
+  {% endif %}
+
+  {{ children }}
+  {% if suffix is not empty %}
+    <span class="field-suffix">{{ suffix }}</span>
+  {% endif %}
+  {# The label is always display after to display checkbox. #}
+  {{ label }}
+  {# In case of we need to display errors in form element. #}
+  {% if show_error %}
+    {% include '@dsfr4drupal/templates/form/form-element.error.html.twig' %}
+  {% endif %}
+</div>
diff --git a/templates/form/form-element--radio.html.twig b/templates/form/form-element--radio.html.twig
new file mode 100644
index 0000000..74456c6
--- /dev/null
+++ b/templates/form/form-element--radio.html.twig
@@ -0,0 +1 @@
+{% include 'form-element--checkbox.html.twig' %}
diff --git a/templates/form/form-element.html.twig b/templates/form/form-element.html.twig
index adee85c..c436fac 100644
--- a/templates/form/form-element.html.twig
+++ b/templates/form/form-element.html.twig
@@ -4,7 +4,7 @@
   'js-form-type-' ~ type|clean_class,
   'form-item-' ~ name|clean_class,
   'js-form-item-' ~ name|clean_class,
-  title_display not in ['after', 'before'] ? 'form-no-label',
+  title_display not in ['after', 'before'] ? 'form-item--no-label',
   disabled == 'disabled' ? 'form-disabled',
 ] %}
 
diff --git a/templates/media/media--media-library.html.twig b/templates/media/media--media-library.html.twig
new file mode 100644
index 0000000..9f70bd2
--- /dev/null
+++ b/templates/media/media--media-library.html.twig
@@ -0,0 +1,20 @@
+{% if attributes is not empty %}
+<div{{ attributes }}>
+{% endif %}
+
+  <div{{ preview_attributes.addClass('js-media-library-item-preview') }}>
+    {{ include('dsfr4drupal:card', {
+      'image': content.thumbnail ? content.thumbnail : '',
+      'title': name,
+      'title_attributes': metadata_attributes,
+      'description': content|without('thumbnail'),
+      'no_arrow': true,
+      'detail': not status ? 'unpublished'|t : '',
+      'detail_icon': 'fr-icon-warning-fill',
+      'variant': 'sm',
+    }, with_context=false) }}
+  </div>
+
+{% if attributes is not empty %}
+  </div>
+{% endif %}
diff --git a/templates/views/views-view--media-library.html.twig b/templates/views/views-view--media-library.html.twig
new file mode 100644
index 0000000..0b66c96
--- /dev/null
+++ b/templates/views/views-view--media-library.html.twig
@@ -0,0 +1,45 @@
+
+{%
+  set classes = [
+  dom_id ? 'js-view-dom-id-' ~ dom_id,
+]
+%}
+<div{{ attributes.addClass(classes) }}>
+  {{ title_prefix }}
+  {{ title }}
+  {{ title_suffix }}
+
+  {% if header %}
+    <header class="fr-tabs">
+      <ul class="fr-tabs__list" role="tablist">
+        {% for link in header %}
+          <li role="presentation">
+            {{ link }}
+          </li>
+        {% endfor %}
+      </ul>
+    </header>
+    {{ attach_library('dsfr4drupal/component.tab') }}
+  {% endif %}
+
+  {{ exposed }}
+  {{ attachment_before }}
+
+  {% if rows -%}
+    {{ rows }}
+  {% elseif empty -%}
+    {{ empty }}
+  {% endif %}
+  {{ pager }}
+
+  {{ attachment_after }}
+  {{ more }}
+
+  {% if footer %}
+    <footer>
+      {{ footer }}
+    </footer>
+  {% endif %}
+
+  {{ feed_icons }}
+</div>
diff --git a/templates/views/views-view-unformatted--media-library.html.twig b/templates/views/views-view-unformatted--media-library.html.twig
new file mode 100644
index 0000000..61fa26d
--- /dev/null
+++ b/templates/views/views-view-unformatted--media-library.html.twig
@@ -0,0 +1,15 @@
+{% set row_classes = [
+  default_row_class ? 'views-row',
+] %}
+
+{% if title %}
+  <h3>{{ title }}</h3>
+{% endif %}
+
+<div class="fr-grid-row fr-grid-row--gutters">
+  {% for row in rows %}
+    <div{{ row.attributes.addClass(row_classes).addClass(['fr-col-12', 'fr-col-sm-6', 'fr-col-md-4', 'fr-col-lg-3']) }}>
+      {{- row.content -}}
+    </div>
+  {% endfor %}
+</div>
-- 
GitLab


From 4d518fd0dbab881097c2afd18a227aea65a70196 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sat, 25 Jan 2025 19:11:56 +0100
Subject: [PATCH 05/45] Increase special inputs rendering.

---
 css/component/form.css | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/css/component/form.css b/css/component/form.css
index 51e959b..176aa97 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -42,3 +42,11 @@ main[role="main"] > .fr-container--fluid > form {
   font-weight: 700;
 }
 
+.js-filter-wrapper {
+  margin-top: calc(var(--form-item-spacing) * -.75);
+}
+
+.js input.form-autocomplete {
+  padding-right: 2.5rem;
+  background-position: calc(100% - 1rem) 50%;
+}
-- 
GitLab


From 77e5fefd9483501e0e910ca82bb8170083af475a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sat, 25 Jan 2025 20:43:12 +0100
Subject: [PATCH 06/45] Manage styles for UI dialog.

---
 css/component/checkbox.css      |  2 +-
 css/component/form.css          |  6 ++--
 css/component/node-preview.css  |  4 +--
 css/component/tabledrag.css     |  2 +-
 css/component/toolbar.css       |  2 +-
 css/component/ui-dialog.css     | 59 +++++++++++++++++++++++++++++++++
 css/component/vertical-tabs.css |  2 +-
 css/theme/card.css              |  2 +-
 css/theme/media-library.css     |  4 +--
 css/theme/pager.css             |  2 +-
 css/theme/tile.css              |  2 +-
 css/theme/tooltip.css           |  2 +-
 dsfr4drupal.info.yml            |  2 ++
 dsfr4drupal.libraries.yml       |  6 ++++
 14 files changed, 82 insertions(+), 15 deletions(-)
 create mode 100644 css/component/ui-dialog.css

diff --git a/css/component/checkbox.css b/css/component/checkbox.css
index be72fa3..f41a5f5 100644
--- a/css/component/checkbox.css
+++ b/css/component/checkbox.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Manage checkbox styles.
+ * Manage styles for checkbox.
  */
 
 /* Fix checkbox rendering when label is visually hidden */
diff --git a/css/component/form.css b/css/component/form.css
index 176aa97..c85a3f4 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Manage form styles.
+ * Manage styles for form.
  */
 
 :root {
@@ -30,12 +30,12 @@ main[role="main"] > .fr-container--fluid > form {
   flex-wrap: wrap;
   margin-block: var(--form-spacing);
   padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing) var(--form-spacing);
-  border: 1px solid var(--border-plain-grey);
+  border: 1px solid var(--border-default-grey);
 }
 
 .fr-upload-group {
   padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing) var(--form-spacing);
-  border: 1px solid var(--border-plain-grey);
+  border: 1px solid var(--border-default-grey);
 }
 
 .fr-upload-group > label {
diff --git a/css/component/node-preview.css b/css/component/node-preview.css
index bf08321..860c39f 100644
--- a/css/component/node-preview.css
+++ b/css/component/node-preview.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Manage node preview styling.
+ * Manage styles for node preview.
  */
 
 .node-preview-container {
@@ -8,7 +8,7 @@
   background-position: 0 100%;
   background-repeat: no-repeat;
   background-size: 100% .25rem;
-  background-image: linear-gradient(0deg,var(--border-plain-grey),var(--border-plain-grey));
+  background-image: linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey));
   padding: 2rem 2rem 2.25rem;
   /* Based on DSFR header z-index. */
   z-index: calc(var(--ground) + 751);
diff --git a/css/component/tabledrag.css b/css/component/tabledrag.css
index 55733fc..90a7957 100644
--- a/css/component/tabledrag.css
+++ b/css/component/tabledrag.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Manage tabledrag styling.
+ * Manage styles for tabledrag.
  */
 
 a.tabledrag-handle[href] {
diff --git a/css/component/toolbar.css b/css/component/toolbar.css
index 1989351..235df43 100644
--- a/css/component/toolbar.css
+++ b/css/component/toolbar.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Fix toolbar styling.
+ * Manage styles for toolbar.
  */
 
 .toolbar a {
diff --git a/css/component/ui-dialog.css b/css/component/ui-dialog.css
new file mode 100644
index 0000000..50972f9
--- /dev/null
+++ b/css/component/ui-dialog.css
@@ -0,0 +1,59 @@
+/**
+ * @file
+ * Manage styles for UI dialog.
+ */
+
+.ui-dialog {
+  padding: 0;
+}
+
+.ui-dialog .ui-dialog-titlebar {
+  padding: calc(var(--form-spacing) / 2) var(--form-spacing);
+}
+
+.ui-widget-header {
+  margin: -1px -1px 0 -1px;
+  background-color: var(--background-flat-blue-france);
+  color: var(--text-inverted-blue-france);
+  font-weight: 700;
+  border: 0;
+}
+
+.ui-widget-content {
+  background: var(--background-default-grey);
+  color: var(--text-default-grey);
+}
+.ui-widget.ui-widget-content {
+  border: 1px solid var(--border-default-grey);
+}
+
+.ui-dialog .ui-dialog-titlebar-close {
+  --icon-size: 2rem;
+
+  width: var(--icon-size);
+  height: var(--icon-size);
+  margin-top: calc(var(--icon-size) / -2);
+  right: var(--form-spacing);
+  background: none;
+  color: var(--text-inverted-blue-france);
+  border: 0;
+  opacity: .8;
+  transition: opacity .25s;
+  overflow: hidden;
+}
+.ui-dialog .ui-dialog-titlebar-close:hover {
+  background: none;
+  opacity: 1;
+}
+
+.ui-dialog .ui-dialog-titlebar-close .ui-button-icon {
+  position: static;
+  display: block;
+  width: 100%;
+  height: 100%;
+  margin: 0;
+  background-image: none;
+  background-color: currentColor;
+  mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0ibTEyIDEwLjYgNC45NS00Ljk2IDEuNCAxLjRMMTMuNDIgMTJsNC45NiA0Ljk1LTEuNCAxLjRMMTIgMTMuNDJsLTQuOTUgNC45Ni0xLjQtMS40TDEwLjU4IDEyIDUuNjMgNy4wNWwxLjQtMS40eiIvPjwvc3ZnPg==);
+  mask-size: contain;
+}
diff --git a/css/component/vertical-tabs.css b/css/component/vertical-tabs.css
index be39efc..b2decff 100644
--- a/css/component/vertical-tabs.css
+++ b/css/component/vertical-tabs.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Manage vertical tabs styling.
+ * Manage styles for vertical tabs.
  */
 
 .vertical-tabs__menu [href] {
diff --git a/css/theme/card.css b/css/theme/card.css
index 4792584..32f5bf9 100644
--- a/css/theme/card.css
+++ b/css/theme/card.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Fix card styling with "time" html tag.
+ * Fix styles for card with "time" html tag.
  */
 
 .fr-card__detail time {
diff --git a/css/theme/media-library.css b/css/theme/media-library.css
index 6acf699..906dc2a 100644
--- a/css/theme/media-library.css
+++ b/css/theme/media-library.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Manage media library styles.
+ * Manage styles for media library.
  */
 
 .media-library-content {
@@ -17,7 +17,7 @@
 
 .js-media-library-add-form-added-media li {
   padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing) var(--form-spacing);
-  border: 1px solid var(--border-plain-grey);
+  border: 1px solid var(--border-default-grey);
 }
 
 .js-media-library-add-form-added-media img {
diff --git a/css/theme/pager.css b/css/theme/pager.css
index a1aa43f..ca2627c 100644
--- a/css/theme/pager.css
+++ b/css/theme/pager.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Fix pagination to allow AJAX use.
+ * Fix styles for pagination to allow AJAX use.
  */
 
 .fr-pagination__link[href=""],
diff --git a/css/theme/tile.css b/css/theme/tile.css
index 494eca3..13c9ba4 100644
--- a/css/theme/tile.css
+++ b/css/theme/tile.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Fix card styling with "time" html tag.
+ * Fix styles for tile with "time" html tag.
  */
 
 .fr-tile__detail time {
diff --git a/css/theme/tooltip.css b/css/theme/tooltip.css
index c7fac6d..9d75cb1 100644
--- a/css/theme/tooltip.css
+++ b/css/theme/tooltip.css
@@ -1,6 +1,6 @@
 /**
  * @file
- * Fix tooltip styling.
+ * Fix styles for tooltip.
  */
 
 .fr-btn--tooltip {
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index 30ba970..8d38db5 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -36,6 +36,8 @@ libraries:
   - dsfr4drupal/utility
 
 libraries-extend:
+  core/drupal.dialog:
+    - dsfr4drupal/drupal.dialog
   core/drupal.form:
     - dsfr4drupal/drupal.form
   core/drupal.message:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index bdaf068..0176457 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -546,6 +546,12 @@ core:
       production: true
       verbose: true
 
+drupal.dialog:
+  css:
+    component:
+      # Need to fix weight to 99 to override "Gin toolbar" styles.
+      css/component/ui-dialog.css: { weight: 100 }
+
 drupal.form:
   css:
     component:
-- 
GitLab


From c36f3392e028df5e609396dbad7cbb74feec8c21 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sun, 26 Jan 2025 10:35:28 +0100
Subject: [PATCH 07/45] Reset Drupal custom form styles for DSFR default forms.

---
 css/component/form.css                 | 11 +++++++++--
 css/{component => theme}/ui-dialog.css |  0
 dsfr4drupal.libraries.yml              |  4 ++--
 3 files changed, 11 insertions(+), 4 deletions(-)
 rename css/{component => theme}/ui-dialog.css (100%)

diff --git a/css/component/form.css b/css/component/form.css
index c85a3f4..a84309d 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -5,8 +5,8 @@
 
 :root {
   --form-spacing: 1.5rem;
-  --form-spacing-s: 1rem;
-  --form-spacing-xs: .5rem;
+  --form-spacing-s: calc(var(--form-spacing) * .67);
+  --form-spacing-xs: calc(var(--form-spacing) / 3);
   --form-item-spacing: var(--form-spacing);
 
   /* Label font-size * line-height */
@@ -15,6 +15,13 @@
   --form-label-input-spacing: .5rem;
 }
 
+/* Unset form spacing for default DSFR forms. */
+.fr-follow__newsletter,
+.fr-search-bar {
+  --form-spacing: 0;
+  --form-item-spacing: 0;
+}
+
 main[role="main"] > .fr-container > form,
 main[role="main"] > .fr-container--fluid > form {
   margin-block: var(--form-spacing);
diff --git a/css/component/ui-dialog.css b/css/theme/ui-dialog.css
similarity index 100%
rename from css/component/ui-dialog.css
rename to css/theme/ui-dialog.css
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index 0176457..7509194 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -548,9 +548,9 @@ core:
 
 drupal.dialog:
   css:
-    component:
+    theme:
       # Need to fix weight to 99 to override "Gin toolbar" styles.
-      css/component/ui-dialog.css: { weight: 100 }
+      css/theme/ui-dialog.css: { weight: 100 }
 
 drupal.form:
   css:
-- 
GitLab


From 722e8cae3e1a6bab96bf6b0288d13a603b6dccf8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sun, 26 Jan 2025 10:51:32 +0100
Subject: [PATCH 08/45] Increase transcription management.

---
 components/modal/modal.component.yml                 |  6 ++++++
 components/modal/modal.twig                          |  6 +++---
 components/transcription/transcription.component.yml | 10 ++++++++++
 components/transcription/transcription.twig          |  9 ++++++---
 4 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/components/modal/modal.component.yml b/components/modal/modal.component.yml
index 183171f..a3f2974 100644
--- a/components/modal/modal.component.yml
+++ b/components/modal/modal.component.yml
@@ -15,6 +15,12 @@ props:
       type: string
       title: Button title attribute
       description: 'By default: "Close", or the translated string in another language.'
+    content:
+      type: string
+      title: Content
+    footer:
+      type: string
+      title: Footer
     html_id:
       type: string
       title: HTML identifier
diff --git a/components/modal/modal.twig b/components/modal/modal.twig
index 6acc70c..6bef3ef 100644
--- a/components/modal/modal.twig
+++ b/components/modal/modal.twig
@@ -20,13 +20,13 @@
               {% endblock modal_title %}
             </div>
             {% block modal_content %}
-              <p>{{ 'Default content.'|t }}</p>
+              {{ content }}
             {% endblock modal_content %}
           </div>
-          {% if modal_footer or block('modal_footer') is defined %}
+          {% if footer or block('modal_footer') is defined %}
             <div class="fr-modal__footer">
               {% block modal_footer %}
-                {{ modal_footer }}
+                {{ footer }}
               {% endblock modal_footer %}
             </div>
           {% endif %}
diff --git a/components/transcription/transcription.component.yml b/components/transcription/transcription.component.yml
index 3c89bd4..0c0f6d8 100644
--- a/components/transcription/transcription.component.yml
+++ b/components/transcription/transcription.component.yml
@@ -7,10 +7,20 @@ props:
     attributes:
       type: Drupal\Core\Template\Attribute
       title: Attributes
+    button_label:
+      type: string
+      title: Button label
+    button_title:
+      type: string
+      title: Button title attribute
     content:
       type: string
       title: Content
       description: Display into transcription modal.
+    expanded:
+      type: boolean
+      title: Is expanded?
+      default: false
     modal_id:
       type: string
       title: Modal identifier
diff --git a/components/transcription/transcription.twig b/components/transcription/transcription.twig
index e47af06..150816e 100644
--- a/components/transcription/transcription.twig
+++ b/components/transcription/transcription.twig
@@ -1,20 +1,23 @@
 {% set attributes = attributes|default(create_attribute()) %}
+{% set button_label = button_label|default('Enlarge'|t) %}
+{% set button_title = button_title|default('Enlarge transcription'|t) %}
 {% set title = title|default('Transcription'|t) %}
 {% set transcription_id = transcription_id|default('transcription-' ~ random()) %}
 {% set collapse_id = 'fr-transcription-collapse-' ~ transcription_id %}
 {% set modal_id = 'fr-transcription-modal-' ~ transcription_id %}
 {% set modal_title = modal_title|default(title) %}
+{% set expanded = expanded ?? false %}
 
 <div{{ attributes.addClass('fr-transcription') }}>
-  <button type="button" class="fr-transcription__btn" aria-expanded="false" aria-controls="{{ collapse_id }}">
+  <button type="button" class="fr-transcription__btn" aria-expanded="{{ expanded ? 'true': 'false' }}" aria-controls="{{ collapse_id }}">
     {{ title }}
   </button>
   <div class="fr-collapse" id="{{ collapse_id }}">
 
     <div class="fr-transcription__footer">
       <div class="fr-transcription__actions-group">
-        <button type="button" class="fr-btn fr-btn--fullscreen" aria-controls="{{ modal_id }}" data-fr-opened="false" title="{{ 'Enlarge transcription'|t }}">
-          {{ 'Enlarge'|t }}
+        <button type="button" class="fr-btn fr-btn--fullscreen" aria-controls="{{ modal_id }}" data-fr-opened="false" title="{{ button_title }}">
+          {{ button_label }}
         </button>
       </div>
     </div>
-- 
GitLab


From ee91560a19de2299226bf0ce5b15f5846e3638b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sun, 26 Jan 2025 11:32:38 +0100
Subject: [PATCH 09/45] Fix modal footer display.

---
 components/modal/modal.component.yml | 5 ++++-
 components/modal/modal.twig          | 8 +++++---
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/components/modal/modal.component.yml b/components/modal/modal.component.yml
index a3f2974..e2dff3c 100644
--- a/components/modal/modal.component.yml
+++ b/components/modal/modal.component.yml
@@ -27,10 +27,13 @@ props:
     title:
       type: string
       title: Title
+    title_tag:
+      type: string
+      title: Title HTML tag
+      default: h1
 slots:
   modal_content:
     title: Modal content
-    required: true
   modal_footer:
     title: Modal footer
   modal_title:
diff --git a/components/modal/modal.twig b/components/modal/modal.twig
index 6bef3ef..652e233 100644
--- a/components/modal/modal.twig
+++ b/components/modal/modal.twig
@@ -2,6 +2,7 @@
 {% set button_label = button_label|default('Close'|t) %}
 {% set button_title = button_title|default('Close'|t) %}
 {% set html_id = html_id|default('modal-default') %}
+{% set title_tag = title_tag|default('h1') %}
 
 <dialog id="{{ html_id }}" class="fr-modal" role="dialog" aria-labelledby="{{ html_id ~ '-title' }}">
   <div class="fr-container fr-container--fluid fr-container-md">
@@ -14,16 +15,17 @@
             </button>
           </div>
           <div class="fr-modal__content">
-            <div id="{{ html_id ~ '-title' }}" class="fr-modal__title">
+            <{{ title_tag }} id="{{ html_id ~ '-title' }}" class="fr-modal__title">
               {% block modal_title %}
                 {{ title }}
               {% endblock modal_title %}
-            </div>
+            </{{ title_tag }}>
             {% block modal_content %}
               {{ content }}
             {% endblock modal_content %}
           </div>
-          {% if footer or block('modal_footer') is defined %}
+
+          {% if footer or (block('modal_footer') is defined and block('modal_footer')|trim is not empty) %}
             <div class="fr-modal__footer">
               {% block modal_footer %}
                 {{ footer }}
-- 
GitLab


From 6ae48b87b7d7c2a4be8701c5f9bd5d6783c05e2b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sun, 26 Jan 2025 12:07:20 +0100
Subject: [PATCH 10/45] Try as much as possible to style UI Dialog like DSFR
 modal.

---
 css/component/form.css      |  3 +-
 css/theme/media-library.css |  5 ++
 css/theme/ui-dialog.css     | 91 +++++++++++++++++++++++++------------
 3 files changed, 69 insertions(+), 30 deletions(-)

diff --git a/css/component/form.css b/css/component/form.css
index a84309d..1a7765f 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -28,7 +28,8 @@ main[role="main"] > .fr-container--fluid > form {
 }
 
 .form-actions,
-.form-item {
+.form-item,
+.form-wrapper {
   margin-block: var(--form-item-spacing);
 }
 
diff --git a/css/theme/media-library.css b/css/theme/media-library.css
index 906dc2a..08498a8 100644
--- a/css/theme/media-library.css
+++ b/css/theme/media-library.css
@@ -7,6 +7,11 @@
   padding: 1em;
 }
 
+/* Remove margin at the end of the form */
+.media-library-view .views-form {
+  margin-bottom: calc(var(--form-spacing) * -1);
+}
+
 .js-media-library-item {
   margin-block: var(--form-item-spacing) !important;
 }
diff --git a/css/theme/ui-dialog.css b/css/theme/ui-dialog.css
index 50972f9..f8e44b7 100644
--- a/css/theme/ui-dialog.css
+++ b/css/theme/ui-dialog.css
@@ -1,22 +1,28 @@
 /**
  * @file
  * Manage styles for UI dialog.
+ * Try as much as possible to reproduce the styles of the DSFR modal.
  */
 
 .ui-dialog {
-  padding: 0;
+  padding: 0;  max-height: 80vh !important;
+  filter: drop-shadow(var(--lifted-shadow));
 }
 
 .ui-dialog .ui-dialog-titlebar {
-  padding: calc(var(--form-spacing) / 2) var(--form-spacing);
+  margin: 0;
+  padding: 4rem 2rem 0;
 }
 
-.ui-widget-header {
-  margin: -1px -1px 0 -1px;
-  background-color: var(--background-flat-blue-france);
-  color: var(--text-inverted-blue-france);
-  font-weight: 700;
-  border: 0;
+.ui-dialog .ui-dialog-title {
+  --title-spacing: 0 0 1rem 0;
+
+  margin: var(--title-spacing);
+}
+
+.ui-dialog .ui-dialog-content {
+  margin-bottom: 4rem;
+  padding: 0 2rem;
 }
 
 .ui-widget-content {
@@ -24,36 +30,63 @@
   color: var(--text-default-grey);
 }
 .ui-widget.ui-widget-content {
-  border: 1px solid var(--border-default-grey);
+  border: 0;
 }
 
-.ui-dialog .ui-dialog-titlebar-close {
-  --icon-size: 2rem;
+.ui-widget-header {
+  background: none;
+  border: 0;
+  color: var(--text-title-grey);
+  font-size: 1.375rem;
+  font-weight: 700;
+}
 
-  width: var(--icon-size);
-  height: var(--icon-size);
-  margin-top: calc(var(--icon-size) / -2);
-  right: var(--form-spacing);
+@media (min-width: 48em) {
+  .ui-widget-header {
+    font-size: 1.5rem;
+    line-height: 2rem;
+  }
+}
+
+.ui-dialog .ui-dialog-titlebar-close {
+  top: 1rem;
+  right: 2rem;
+  margin: 0;
+  padding: .25rem .75rem;
+  width: auto;
+  height: auto;
+  min-height: 2rem;
   background: none;
-  color: var(--text-inverted-blue-france);
   border: 0;
-  opacity: .8;
-  transition: opacity .25s;
-  overflow: hidden;
+  text-align: right;
+  text-indent: initial;
+  color: var(--text-action-high-blue-france);
+  font-size: .875rem;
+  font-weight: 500;
+  line-height: 1.5rem;
+  overflow: initial;
 }
 .ui-dialog .ui-dialog-titlebar-close:hover {
-  background: none;
-  opacity: 1;
+  background-color: var(--hover-tint);
 }
 
-.ui-dialog .ui-dialog-titlebar-close .ui-button-icon {
-  position: static;
-  display: block;
-  width: 100%;
-  height: 100%;
-  margin: 0;
-  background-image: none;
+.ui-dialog .ui-dialog-titlebar-close::after {
+  --icon-size: 1rem;
+
   background-color: currentColor;
+  content: '';
+  display: inline-block;
+  flex: 0 0 auto;
+  height: var(--icon-size);
+  margin-left: .5rem;
+  margin-right: -.125rem;
   mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0ibTEyIDEwLjYgNC45NS00Ljk2IDEuNCAxLjRMMTMuNDIgMTJsNC45NiA0Ljk1LTEuNCAxLjRMMTIgMTMuNDJsLTQuOTUgNC45Ni0xLjQtMS40TDEwLjU4IDEyIDUuNjMgNy4wNWwxLjQtMS40eiIvPjwvc3ZnPg==);
-  mask-size: contain;
+  mask-size: 100% 100%;
+  vertical-align: calc((.75em - var(--icon-size))*.5);
+  width: var(--icon-size);
+}
+
+.ui-dialog .ui-dialog-titlebar-close .ui-button-icon,
+.ui-dialog .ui-dialog-titlebar-close  .ui-button-icon-space {
+  display: none;
 }
-- 
GitLab


From 171cf5197cdd8ce92dcce09d8d717c074b690958 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sun, 26 Jan 2025 12:11:18 +0100
Subject: [PATCH 11/45] Provide a custom class to remove form spacing.

---
 css/component/form.css | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/css/component/form.css b/css/component/form.css
index 1a7765f..5f0ee52 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -15,6 +15,12 @@
   --form-label-input-spacing: .5rem;
 }
 
+/* Provide a class to remove spacing. */
+.dsfr4drupal-form--no-spacing {
+  --form-spacing: 0;
+  --form-item-spacing: 0;
+}
+
 /* Unset form spacing for default DSFR forms. */
 .fr-follow__newsletter,
 .fr-search-bar {
-- 
GitLab


From 648f0d701eadf4b327c507a932578487be1edb00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Sun, 26 Jan 2025 12:12:17 +0100
Subject: [PATCH 12/45] Fix missing file.

---
 css/{ => theme}/navigation.header.css | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename css/{ => theme}/navigation.header.css (100%)

diff --git a/css/navigation.header.css b/css/theme/navigation.header.css
similarity index 100%
rename from css/navigation.header.css
rename to css/theme/navigation.header.css
-- 
GitLab


From 2582170db2e215756ab75df7fd54fba9f18bfc0a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Mon, 27 Jan 2025 11:50:42 +0100
Subject: [PATCH 13/45] Finalize form table and tabledrag rendering.

---
 css/component/form.css      | 15 +++++++++++++++
 css/component/tabledrag.css |  9 +++++++++
 js/tabledrag.js             | 18 ++++++++++++++++++
 3 files changed, 42 insertions(+)

diff --git a/css/component/form.css b/css/component/form.css
index 5f0ee52..9a116e2 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -39,6 +39,21 @@ main[role="main"] > .fr-container--fluid > form {
   margin-block: var(--form-item-spacing);
 }
 
+/* Disable table scrolling into form item - START */
+.form-item .fr-table__container {
+  overflow: initial;
+}
+
+.form-item .fr-table > table caption {
+  max-width: calc(100vw - 2rem);
+}
+
+.form-item .fr-table .fr-table__wrapper .fr-table__content table th,
+.form-item .fr-table .fr-table__wrapper .fr-table__content table td {
+  white-space: normal;
+}
+/* Disable table scrolling into form - END */
+
 .views-exposed-form {
   display: flex;
   flex-wrap: wrap;
diff --git a/css/component/tabledrag.css b/css/component/tabledrag.css
index 90a7957..76f5569 100644
--- a/css/component/tabledrag.css
+++ b/css/component/tabledrag.css
@@ -20,6 +20,11 @@ a.tabledrag-handle .handle {
   background-position: center center;
 }
 
+.draggable.drag td,
+.draggable.drag th {
+  background-color: var(--background-default-grey-active);
+}
+
 .field-multiple-drag {
   max-width: fit-content;
 }
@@ -31,3 +36,7 @@ a.tabledrag-handle .handle {
 .tabledrag-toggle-weight-wrapper + .fr-table {
   margin-top: 0;
 }
+
+.tabledrag-changed-warning {
+  color: var(--warning-425-625);
+}
diff --git a/js/tabledrag.js b/js/tabledrag.js
index 8d38816..c44218f 100644
--- a/js/tabledrag.js
+++ b/js/tabledrag.js
@@ -20,6 +20,24 @@
   );
 
   const initColumnsOriginal = Drupal.tableDrag.prototype.initColumns;
+  const addChangedWarningOriginal = Drupal.tableDrag.prototype.row.prototype.addChangedWarning;
+
+  /**
+   @inheritDoc
+   */
+  Drupal.tableDrag.prototype.row.prototype.addChangedWarning = function () {
+    const $table = $(this.table.parentNode);
+
+    // Do not add the changed warning if one is already present.
+    if (!$table.find('.tabledrag-changed-warning').length) {
+      addChangedWarningOriginal.call(this);
+
+      const $tableWrapper = $table.parents(".fr-table");
+
+      // Move changed warning message before DSFR table wrapper.
+      $tableWrapper.parent().prepend($table.find('.tabledrag-changed-warning'));
+    }
+  };
 
   /**
    * @inheritDoc
-- 
GitLab


From b87d063eb159d73b764cc4a47d2003b71dc15efd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Mon, 27 Jan 2025 17:16:57 +0100
Subject: [PATCH 14/45] Manage form groups styles.

---
 components/accordion/accordion.component.yml  |  3 +
 components/accordion/accordion.twig           | 11 ++-
 css/component/form.css                        | 13 ++-
 css/component/vertical-tabs.css               | 16 ----
 css/theme/horizontal-tabs.css                 | 61 +++++++++++++
 css/theme/media-library.css                   |  2 +-
 css/theme/vertical-tabs.css                   | 49 +++++++++++
 dsfr4drupal.info.yml                          |  2 +
 dsfr4drupal.libraries.yml                     | 17 +++-
 dsfr4drupal.theme                             | 14 +++
 includes/form.theme                           | 21 +++++
 js/accordion.js                               | 22 +++++
 js/horizontal-tabs.js                         | 55 ++++++++++++
 js/tabledrag.js                               |  3 +-
 src/Dsfr4DrupalPreRender.php                  | 87 +++++++++++++++++++
 .../form/details--horizontal-tabs.html.twig   |  2 +
 .../form/details--vertical-tabs.html.twig     |  2 +
 templates/form/horizontal-tabs.html.twig      |  6 ++
 templates/system/details.html.twig            | 16 ++++
 19 files changed, 376 insertions(+), 26 deletions(-)
 delete mode 100644 css/component/vertical-tabs.css
 create mode 100644 css/theme/horizontal-tabs.css
 create mode 100644 css/theme/vertical-tabs.css
 create mode 100644 js/accordion.js
 create mode 100644 js/horizontal-tabs.js
 create mode 100644 src/Dsfr4DrupalPreRender.php
 create mode 100644 templates/form/details--horizontal-tabs.html.twig
 create mode 100644 templates/form/details--vertical-tabs.html.twig
 create mode 100644 templates/form/horizontal-tabs.html.twig
 create mode 100644 templates/system/details.html.twig

diff --git a/components/accordion/accordion.component.yml b/components/accordion/accordion.component.yml
index 5c1a7a2..9b05443 100644
--- a/components/accordion/accordion.component.yml
+++ b/components/accordion/accordion.component.yml
@@ -10,6 +10,9 @@ props:
     attributes:
       type: Drupal\Core\Template\Attribute
       title: Attributes
+    button_attributes:
+      type: Drupal\Core\Template\Attribute
+      title: Button attributes
     content:
       type: string
       title: Content
diff --git a/components/accordion/accordion.twig b/components/accordion/accordion.twig
index 1b4df84..ec6060f 100644
--- a/components/accordion/accordion.twig
+++ b/components/accordion/accordion.twig
@@ -1,13 +1,16 @@
 {% set accordion_id = accordion_id|default('accordion-' ~ random()) %}
 {% set attributes = attributes|default(create_attribute()) %}
+{% set button_attributes = button_attributes|default(create_attribute()) %}
 {% set title_tag = title_tag|default('h3') %}
 
 <section{{ attributes.addClass('fr-accordion') }}>
   <{{ title_tag }} class="fr-accordion__title">
-    <button class="fr-accordion__btn"
-            aria-expanded="{{ expanded ? 'true' : 'false' }}"
-            aria-controls="{{ accordion_id }}"
-            type="button">
+    <button{{ button_attributes
+      .addClass('fr-accordion__btn')
+      .setAttribute('type', button)
+      .setAttribute('aria-expanded', expanded ? 'true' : 'false')
+      .setAttribute('aria-controls', accordion_id) }}
+    >
       {{ title }}
     </button>
   </{{ title_tag }}>
diff --git a/css/component/form.css b/css/component/form.css
index 9a116e2..a61b3b6 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -58,12 +58,12 @@ main[role="main"] > .fr-container--fluid > form {
   display: flex;
   flex-wrap: wrap;
   margin-block: var(--form-spacing);
-  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing) var(--form-spacing);
+  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing);
   border: 1px solid var(--border-default-grey);
 }
 
 .fr-upload-group {
-  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing) var(--form-spacing);
+  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing);
   border: 1px solid var(--border-default-grey);
 }
 
@@ -79,3 +79,12 @@ main[role="main"] > .fr-container--fluid > form {
   padding-right: 2.5rem;
   background-position: calc(100% - 1rem) 50%;
 }
+
+.paragraphs-tabs-wrapper {
+  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing);
+  border: 1px solid var(--border-default-grey);
+}
+
+.paragraphs-tabs-wrapper > .form-item {
+  margin-block: 0;
+}
diff --git a/css/component/vertical-tabs.css b/css/component/vertical-tabs.css
deleted file mode 100644
index b2decff..0000000
--- a/css/component/vertical-tabs.css
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * @file
- * Manage styles for vertical tabs.
- */
-
-.vertical-tabs__menu [href] {
-  background-image: none;
-}
-
-.vertical-tabs__pane {
-  padding: 1.5rem;
-}
-
-.vertical-tabs__pane .fr-input {
-  box-sizing: border-box;
-}
diff --git a/css/theme/horizontal-tabs.css b/css/theme/horizontal-tabs.css
new file mode 100644
index 0000000..489e76e
--- /dev/null
+++ b/css/theme/horizontal-tabs.css
@@ -0,0 +1,61 @@
+/**
+ * @file
+ * Manage styles for horizontal tabs.
+ * Try as much as possible to reproduce the styles of the DSFR tabs.
+ */
+
+.horizontal-tabs {
+  margin: 0 0 var(--form-spacing);
+  border: 0;
+}
+
+.horizontal-tabs .horizontal-tabs-list {
+  /* Remove uwanted styles */
+  background: none;
+  border: 0;
+
+  /* Force DSFR tabs styles */
+  display: flex;
+  margin: -4px 0;
+  padding: 4px .75rem;
+}
+
+.horizontal-tabs ul.horizontal-tabs-list li a,
+.horizontal-tabs ul.horizontal-tabs-list li.selected a {
+  /* Force DSFR tabs styles */
+  display: inline-flex;
+  padding: .5rem 1rem;
+  position: relative;
+}
+
+.horizontal-tabs .horizontal-tab-button {
+  /* Remove uwanted styles */
+  float: none;
+  background: none;
+  border: 0;
+  min-width: auto;
+}
+
+.horizontal-tabs .horizontal-tab-button a:hover {
+  /* Force DSFR tabs styles */
+  background-color: var(--hover-tint);
+}
+
+.horizontal-tabs-list.fr-tabs__list + .horizontal-tabs-panes {
+  /* Fix order */
+  order: 3;
+}
+
+.horizontal-tabs-panes {
+  border: 1px solid var(--border-default-grey);
+  border-top: 0;
+  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing-s);
+}
+
+.horizontal-tabs-panes .horizontal-tabs-pane {
+  padding: 0;
+}
+
+.horizontal-tabs-panes .fr-input {
+  box-sizing: border-box;
+}
diff --git a/css/theme/media-library.css b/css/theme/media-library.css
index 08498a8..e1bddc1 100644
--- a/css/theme/media-library.css
+++ b/css/theme/media-library.css
@@ -21,7 +21,7 @@
 }
 
 .js-media-library-add-form-added-media li {
-  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing) var(--form-spacing);
+  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing);
   border: 1px solid var(--border-default-grey);
 }
 
diff --git a/css/theme/vertical-tabs.css b/css/theme/vertical-tabs.css
new file mode 100644
index 0000000..fb663b6
--- /dev/null
+++ b/css/theme/vertical-tabs.css
@@ -0,0 +1,49 @@
+/**
+ * @file
+ * Manage styles for vertical tabs.
+ */
+
+.vertical-tabs__menu [href] {
+  background-image: none;
+}
+
+.vertical-tabs__menu li {
+  --li-bottom: 0;
+}
+
+.vertical-tabs__menu-item a:hover {
+  outline: none;
+}
+
+.vertical-tabs__menu-item:not(.is-selected) a {
+  background-color: var(--background-action-low-blue-france);
+}
+
+.vertical-tabs__menu-item:not(.is-selected) a:hover {
+  background-color: var(--background-action-low-blue-france-hover);
+}
+
+.vertical-tabs__menu-item.is-selected a {
+  background-color: var(--background-default-grey);
+  background-image: linear-gradient(0deg,var(--border-active-blue-france),var(--border-active-blue-france)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey));
+  background-size: 100% 2px,1px calc(100% - 1px),1px calc(100% - 1px),0 1px;
+  color: var(--text-active-blue-france);
+}
+
+.vertical-tabs__menu-item.is-selected .vertical-tabs__menu-item-title {
+  color: inherit;
+}
+
+.vertical-tabs__menu-item a:focus .vertical-tabs__menu-item-title,
+.vertical-tabs__menu-item a:active .vertical-tabs__menu-item-title,
+.vertical-tabs__menu-item a:hover .vertical-tabs__menu-item-title {
+  text-decoration: none;
+}
+
+.vertical-tabs__panes {
+  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing-s);
+}
+
+.vertical-tabs__pane .fr-input {
+  box-sizing: border-box;
+}
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index 8d38db5..fea536a 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -46,6 +46,8 @@ libraries-extend:
     - dsfr4drupal/drupal.tabledrag
   core/drupal.vertical-tabs:
     - dsfr4drupal/drupal.vertical-tabs
+  field_group/element.horizontal_tabs:
+    - dsfr4drupal/element.horizontal_tabs
   media_library/view:
     - dsfr4drupal/media_library.theme
   media_library/widget:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index 7509194..befee60 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -6,7 +6,10 @@ component.accordion:
   css:
     component:
       /libraries/dsfr/dist/component/accordion/accordion.min.css: { minified: true }
+  js:
+    js/accordion.js: {}
   dependencies:
+    - core/once
     - dsfr4drupal/core
 
 component.alert:
@@ -577,8 +580,18 @@ drupal.tabledrag:
 
 drupal.vertical-tabs:
   css:
-    component:
-      css/component/vertical-tabs.css: {}
+    theme:
+      css/theme/vertical-tabs.css: {}
+
+element.horizontal_tabs:
+  css:
+    theme:
+      css/theme/horizontal-tabs.css: {}
+  js:
+    js/horizontal-tabs.js: {}
+  dependencies:
+    - dsfr4drupal/component.tab
+    - core/once
 
 #legacy:
 #  js:
diff --git a/dsfr4drupal.theme b/dsfr4drupal.theme
index 657df94..f2d9a31 100644
--- a/dsfr4drupal.theme
+++ b/dsfr4drupal.theme
@@ -10,6 +10,7 @@ declare(strict_types=1);
 use Drupal\Core\Asset\AttachedAssetsInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\dsfr4drupal\Dsfr4DrupalInterface;
+use Drupal\dsfr4drupal\Dsfr4DrupalPreRender;
 
 // Include all files from the includes directory.
 $includes_path = dirname(__FILE__) . '/includes/*.theme';
@@ -17,6 +18,19 @@ foreach (glob($includes_path) as $file) {
   require_once dirname(__FILE__) . '/includes/' . basename($file);
 }
 
+/**
+ * Implements hook_element_info_alter().
+ */
+function dsfr4drupal_element_info_alter(array &$type): void {
+  if (isset($type['horizontal_tabs'])) {
+    $type['horizontal_tabs']['#pre_render'][] = [Dsfr4DrupalPreRender::class, 'horizontalTabs'];
+  }
+  if (isset($type['vertical_tabs'])) {
+    $type['vertical_tabs']['#pre_render'][] = [Dsfr4DrupalPreRender::class, 'verticalTabs'];
+  }
+
+}
+
 /**
  * Implements hook_preprocess().
  */
diff --git a/includes/form.theme b/includes/form.theme
index d639458..02e5c9c 100644
--- a/includes/form.theme
+++ b/includes/form.theme
@@ -277,6 +277,27 @@ function dsfr4drupal_preprocess_textarea(array &$variables): void {
   }
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK_alter() for details.
+ */
+function dsfr4drupal_theme_suggestions_details_alter(array &$suggestions, array $variables): void {
+  /**
+   * The property "#horizontal_tab_item" is added during element prerender.
+   * @see: \Drupal\dsfr4drupal\Dsfr4DrupalPrerender::horizontalTabs()
+   */
+  if (!empty($variables['element']['#horizontal_tab_item'])) {
+    $suggestions[] = 'details__horizontal_tabs';
+  }
+
+  /**
+   * The property "#vertical_tab_item" is added during element prerender.
+   * @see: \Drupal\dsfr4drupal\Dsfr4DrupalPrerender::verticalTabs()
+   */
+  if (!empty($variables['element']['#vertical_tab_item'])) {
+    $suggestions[] = 'details__vertical_tabs';
+  }
+}
+
 /**
  * Implements hook_theme_suggestions_HOOK_alter() for "form_elemtn".
  */
diff --git a/js/accordion.js b/js/accordion.js
new file mode 100644
index 0000000..14679c1
--- /dev/null
+++ b/js/accordion.js
@@ -0,0 +1,22 @@
+/**
+ * @file
+ * Manage accordion features with DSFR.
+ */
+
+((Drupal, once) => {
+
+  Drupal.behaviors.dsfrAccordion = {
+    attach: (context) => {
+
+      // Search all accordion in current context.
+      once("dsfr-accordion", ".fr-accordion", context).forEach((element) => {
+        // Manage accordion button click event.
+        element.querySelector(".fr-accordion__btn").addEventListener("click", event => {
+          // Disable form submission.
+          event.preventDefault();
+        });
+      });
+    }
+  };
+
+})(Drupal, once);
diff --git a/js/horizontal-tabs.js b/js/horizontal-tabs.js
new file mode 100644
index 0000000..cb1604a
--- /dev/null
+++ b/js/horizontal-tabs.js
@@ -0,0 +1,55 @@
+/**
+ * @file
+ * Manage horizontal tabs features with DSFR.
+ */
+
+((Drupal, once) => {
+
+  const CLASS_LIST = "fr-tabs__list";
+  const CLASS_TAB = "fr-tabs__tab";
+  const CLASS_TAB_SELECTED = "fr-tabs__tab--selected";
+  const CLASS_TABS = "fr-tabs";
+
+  Drupal.behaviors.dsfrHorizontalTabs = {
+    attach: (context) => {
+
+      // Search all horizontal tabs in current context.
+      once("dsfr-horizontal-tabs", ".horizontal-tabs", context).forEach((element) => {
+        const wrapper = element.querySelector(`.${CLASS_TABS}`);
+        if (!wrapper) {
+          return;
+        }
+
+        const list = element.querySelector(`.horizontal-tabs-list.${CLASS_LIST}`);
+        if (!list) {
+          return;
+        }
+
+        wrapper.append(list);
+
+        list.querySelectorAll("li").forEach((item) => {
+
+          const link = item.querySelector("a");
+          link.classList.add(CLASS_TAB);
+
+          if (item.classList.contains("selected")) {
+            link.classList.add(CLASS_TAB_SELECTED);
+            link.setAttribute("aria-selected", "true");
+          }
+
+          link.addEventListener("click", () => {
+            // Remove selected class on old active link.
+            const selected = list.querySelector(`.${CLASS_TAB_SELECTED}`);
+            selected.classList.remove(CLASS_TAB_SELECTED);
+            selected.removeAttribute("aria-selected");
+
+            // Add class to new element.
+            link.classList.add(CLASS_TAB_SELECTED);
+            link.setAttribute("aria-selected", "true");
+          })
+        });
+      });
+    }
+  };
+
+})(Drupal, once);
diff --git a/js/tabledrag.js b/js/tabledrag.js
index c44218f..da730da 100644
--- a/js/tabledrag.js
+++ b/js/tabledrag.js
@@ -1,6 +1,6 @@
 /**
  * @file
- * Provide dragging capabilities to admin uis.
+ * Manage tabbledrag features with DSFR.
  */
 
 /**
@@ -10,6 +10,7 @@
  */
 
 (function ($, Drupal, drupalSettings) {
+
   /**
    * Store the state of weight columns display for all tables.
    *
diff --git a/src/Dsfr4DrupalPreRender.php b/src/Dsfr4DrupalPreRender.php
new file mode 100644
index 0000000..0674233
--- /dev/null
+++ b/src/Dsfr4DrupalPreRender.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\dsfr4drupal;
+
+use Drupal\Core\Render\Element;
+use Drupal\Core\Security\TrustedCallbackInterface;
+
+/**
+ * Implements trusted prerender callbacks for the "DSFR for Drupal" theme.
+ *
+ * @internal
+ */
+class Dsfr4DrupalPreRender implements TrustedCallbackInterface {
+
+  /**
+   * Prerender callback for Horizontal Tabs element.
+   *
+   * @param array $element
+   *   The render array element.
+   *
+   * @return array
+   *     The new render array element.
+   */
+  public static function horizontalTabs(array $element): array {
+    return self::tabs($element, 'horizontal');
+  }
+
+  /**
+   * Prerender callback for Vertical Tabs element.
+   *
+   * @param array $element
+   *    The render array element.
+   *
+   * @return array
+   *    The new render array element.
+   */
+  public static function verticalTabs(array $element): array {
+    return self::tabs($element, 'vertical');
+  }
+
+  /**
+   * Prerender callback for tabs element.
+   *
+   * @param array $element
+   *    The render array element.
+   *
+   * @return array
+   *    The new render array element.
+   */
+  private static function tabs(array $element, string $orientation): array {
+    $isDetails = isset($element['group']['#type']) && $element['group']['#type'] === 'details';
+    $existingGroups = isset($element['group']['#groups']) && is_array($element['group']['#groups']);
+
+    // If the horizontal/vertical tabs have a details group, add attributes to those
+    // details elements so they are styled as accordion items and have BEM classes.
+    if ($isDetails && $existingGroups) {
+      $groupKeys = Element::children($element['group']['#groups'], TRUE);
+
+      $groupKey = implode('][', $element['#parents']);
+      // Only check siblings against groups because we are only looking for
+      // group elements.
+      if (in_array($groupKey, $groupKeys)) {
+        $childrenKeys = Element::children($element['group']['#groups'][$groupKey], TRUE);
+
+        foreach ($childrenKeys as $childKey) {
+          $type = $element['group']['#groups'][$groupKey][$childKey]['#type'] ?? NULL;
+          if ($type === 'details') {
+            $element['group']['#groups'][$groupKey][$childKey]['#' . $orientation . '_tab_item'] = TRUE;
+          }
+        }
+      }
+    }
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function trustedCallbacks(): array {
+    return [
+      'horizontalTabs',
+      'verticalTabs',
+    ];
+  }
+
+}
diff --git a/templates/form/details--horizontal-tabs.html.twig b/templates/form/details--horizontal-tabs.html.twig
new file mode 100644
index 0000000..c5a902d
--- /dev/null
+++ b/templates/form/details--horizontal-tabs.html.twig
@@ -0,0 +1,2 @@
+{# Specific "details" templating for vertical tabs. Do not use DSFR accordion. #}
+{% include '@system/templates/details.html.twig' %}
diff --git a/templates/form/details--vertical-tabs.html.twig b/templates/form/details--vertical-tabs.html.twig
new file mode 100644
index 0000000..c5a902d
--- /dev/null
+++ b/templates/form/details--vertical-tabs.html.twig
@@ -0,0 +1,2 @@
+{# Specific "details" templating for vertical tabs. Do not use DSFR accordion. #}
+{% include '@system/templates/details.html.twig' %}
diff --git a/templates/form/horizontal-tabs.html.twig b/templates/form/horizontal-tabs.html.twig
new file mode 100644
index 0000000..7af90c4
--- /dev/null
+++ b/templates/form/horizontal-tabs.html.twig
@@ -0,0 +1,6 @@
+<div data-horizontal-tabs class="horizontal-tabs">
+  <div class="fr-tabs"></div>
+  {# Cannot add list into "fr-tabs", otherwise it's not populated. #}
+  <ul data-horizontal-tabs-list class="horizontal-tabs-list fr-tabs__list"></ul>
+  <div data-horizontal-tabs-panes{{ attributes }}>{{ children }}</div>
+</div>
diff --git a/templates/system/details.html.twig b/templates/system/details.html.twig
new file mode 100644
index 0000000..991c919
--- /dev/null
+++ b/templates/system/details.html.twig
@@ -0,0 +1,16 @@
+{% set content %}
+  {% if errors %}
+    <div>
+      {{ errors }}
+    </div>
+  {% endif %}
+
+  {{ description }}
+  {{ children }}
+  {{ value }}
+{% endset %}
+
+{{ include('dsfr4drupal:accordion', {
+  'button_attributes': create_attribute({'class': required ? ['js-form-required', 'form-required'] : ''}),
+  'title_tag': 'div',
+}) }}
-- 
GitLab


From 8dfe543f54096dd9fdc919bf4cba25b70f7b3602 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Mon, 27 Jan 2025 18:26:58 +0100
Subject: [PATCH 15/45] Add actions button margin.

---
 css/component/form.css | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/css/component/form.css b/css/component/form.css
index a61b3b6..ceef11d 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -39,6 +39,12 @@ main[role="main"] > .fr-container--fluid > form {
   margin-block: var(--form-item-spacing);
 }
 
+.form-actions a + button,
+.form-actions button + button,
+.form-actions button + a {
+  margin-left: var(--form-item-spacing);
+}
+
 /* Disable table scrolling into form item - START */
 .form-item .fr-table__container {
   overflow: initial;
-- 
GitLab


From d8e2cd51b8a379391b0b0cc6386526913c3e91d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Mon, 27 Jan 2025 18:29:44 +0100
Subject: [PATCH 16/45] Remove too margins.

---
 css/component/form.css | 2 --
 1 file changed, 2 deletions(-)

diff --git a/css/component/form.css b/css/component/form.css
index ceef11d..f612baf 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -33,8 +33,6 @@ main[role="main"] > .fr-container--fluid > form {
   margin-block: var(--form-spacing);
 }
 
-.form-actions,
-.form-item,
 .form-wrapper {
   margin-block: var(--form-item-spacing);
 }
-- 
GitLab


From 7d0bbfbd7c7ca178012fb66d81f52fd60fb414d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 29 Jan 2025 18:02:32 +0100
Subject: [PATCH 17/45] Prepare to manage paragraphs widgets.

---
 css/component/paragraphs-admin.css  | 29 +++++++++++++++++++++
 css/component/paragraphs-widget.css | 39 +++++++++++++++++++++++++++++
 css/theme/horizontal-tabs.css       |  3 ++-
 css/theme/vertical-tabs.css         |  3 ++-
 dsfr4drupal.info.yml                |  4 +++
 dsfr4drupal.libraries.yml           | 10 ++++++++
 6 files changed, 86 insertions(+), 2 deletions(-)
 create mode 100644 css/component/paragraphs-admin.css
 create mode 100644 css/component/paragraphs-widget.css

diff --git a/css/component/paragraphs-admin.css b/css/component/paragraphs-admin.css
new file mode 100644
index 0000000..bcf024f
--- /dev/null
+++ b/css/component/paragraphs-admin.css
@@ -0,0 +1,29 @@
+/**
+ * @file
+ * Manage styles for paragraphs admin.
+ */
+
+.paragraph-type-title {
+  font-size: 1.5em;
+  font-weight: 700;
+  margin-top: var(--form-spacing-xs);
+}
+
+.js .field--widget-entity-reference-paragraphs td {
+  /* Force DSFR table styles */
+  padding: .5rem 1rem;
+}
+
+.js .field--widget-entity-reference-paragraphs .field-multiple-table > thead > tr > th:nth-child(2),
+.js .field--widget-entity-reference-paragraphs .field-multiple-table > tbody > tr > td:nth-child(3) {
+  padding: 0;
+}
+
+.js .field--widget-entity-reference-paragraphs .field-multiple-drag {
+  vertical-align: middle;
+  padding-right: 0;
+}
+
+.paragraphs-tabs-wrapper .field-multiple-table > thead > tr .field-label .paragraphs-actions {
+  margin-inline: auto 0;
+}
diff --git a/css/component/paragraphs-widget.css b/css/component/paragraphs-widget.css
new file mode 100644
index 0000000..6c5f521
--- /dev/null
+++ b/css/component/paragraphs-widget.css
@@ -0,0 +1,39 @@
+/**
+ * @file
+ * Manage styles for paragraphs widget.
+ */
+
+.paragraph-type-label {
+  font-size: 1.5em;
+  font-weight: 700;
+}
+
+.js .field--widget-paragraphs td {
+  /* Force DSFR table styles */
+  padding: .5rem 1rem;
+}
+
+.js .field--widget-paragraphs .field-multiple-drag {
+  min-width: auto;
+}
+
+.js .field--widget-paragraphs .draggable .tabledrag-handle {
+  padding: 0;
+}
+
+.js .field--widget-paragraphs .draggable .field-multiple-drag {
+  padding-right: 0;
+}
+
+.paragraphs-tabs-wrapper .field-multiple-table > thead > tr > th:nth-child(2),
+.paragraphs-tabs-wrapper .field-multiple-table > tbody > tr > td:nth-child(3) {
+  padding: 0;
+}
+
+.paragraphs-tabs-wrapper .field-multiple-table > thead > tr .field-label .label {
+  display: inline-block;
+}
+
+.paragraphs-tabs-wrapper .field-multiple-table > thead > tr .field-label .paragraphs-actions {
+  margin-inline: auto 0;
+}
diff --git a/css/theme/horizontal-tabs.css b/css/theme/horizontal-tabs.css
index 489e76e..c761c2c 100644
--- a/css/theme/horizontal-tabs.css
+++ b/css/theme/horizontal-tabs.css
@@ -56,6 +56,7 @@
   padding: 0;
 }
 
-.horizontal-tabs-panes .fr-input {
+.horizontal-tabs-panes .fr-input,
+.horizontal-tabs-panes .fr-select {
   box-sizing: border-box;
 }
diff --git a/css/theme/vertical-tabs.css b/css/theme/vertical-tabs.css
index fb663b6..43ae05c 100644
--- a/css/theme/vertical-tabs.css
+++ b/css/theme/vertical-tabs.css
@@ -44,6 +44,7 @@
   padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing-s);
 }
 
-.vertical-tabs__pane .fr-input {
+.vertical-tabs__pane .fr-input,
+.vertical-tabs__pane .fr-select {
   box-sizing: border-box;
 }
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index fea536a..3be50c0 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -56,6 +56,10 @@ libraries-extend:
     - dsfr4drupal/navigation.layout
   node/drupal.node.preview:
     - dsfr4drupal/drupal.node.preview
+  paragraphs/drupal.paragraphs.admin:
+    - dsfr4drupal/drupal.paragraphs.admin
+  paragraphs/drupal.paragraphs.widget:
+    - dsfr4drupal/drupal.paragraphs.widget
   tarte_au_citron/tarte_au_citron_lib:
     - dsfr4drupal/tarteaucitron
   tacjs/tarteaucitron.js:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index befee60..da75f1f 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -571,6 +571,16 @@ drupal.node.preview:
     component:
       css/component/node-preview.css: {}
 
+drupal.paragraphs.admin:
+  css:
+    component:
+      css/component/paragraphs-admin.css: { }
+
+drupal.paragraphs.widget:
+  css:
+    component:
+      css/component/paragraphs-widget.css: { }
+
 drupal.tabledrag:
   css:
     component:
-- 
GitLab


From a215b73f1ea004ce256704a7c1ef63a3df8a428e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Thu, 30 Jan 2025 11:21:07 +0100
Subject: [PATCH 18/45] Finalize paragraphs widgets styling.

---
 ...agraphs-admin.css => paragraphs.admin.css} |  0
 ...raphs-widget.css => paragraphs.widget.css} |  6 ++
 css/theme/horizontal-tabs.css                 |  4 +-
 dsfr4drupal.libraries.yml                     | 12 +++-
 js/paragraphs.widget.js                       | 58 +++++++++++++++++++
 5 files changed, 75 insertions(+), 5 deletions(-)
 rename css/component/{paragraphs-admin.css => paragraphs.admin.css} (100%)
 rename css/component/{paragraphs-widget.css => paragraphs.widget.css} (86%)
 create mode 100644 js/paragraphs.widget.js

diff --git a/css/component/paragraphs-admin.css b/css/component/paragraphs.admin.css
similarity index 100%
rename from css/component/paragraphs-admin.css
rename to css/component/paragraphs.admin.css
diff --git a/css/component/paragraphs-widget.css b/css/component/paragraphs.widget.css
similarity index 86%
rename from css/component/paragraphs-widget.css
rename to css/component/paragraphs.widget.css
index 6c5f521..2c29999 100644
--- a/css/component/paragraphs-widget.css
+++ b/css/component/paragraphs.widget.css
@@ -37,3 +37,9 @@
 .paragraphs-tabs-wrapper .field-multiple-table > thead > tr .field-label .paragraphs-actions {
   margin-inline: auto 0;
 }
+
+.is-horizontal .paragraphs-tabs:first-of-type {
+  /* Remove unwanted styles */
+  position: static;
+  background-color: transparent;
+}
diff --git a/css/theme/horizontal-tabs.css b/css/theme/horizontal-tabs.css
index c761c2c..f0d6610 100644
--- a/css/theme/horizontal-tabs.css
+++ b/css/theme/horizontal-tabs.css
@@ -10,7 +10,7 @@
 }
 
 .horizontal-tabs .horizontal-tabs-list {
-  /* Remove uwanted styles */
+  /* Remove unwanted styles */
   background: none;
   border: 0;
 
@@ -29,7 +29,7 @@
 }
 
 .horizontal-tabs .horizontal-tab-button {
-  /* Remove uwanted styles */
+  /* Remove unwanted styles */
   float: none;
   background: none;
   border: 0;
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index da75f1f..5f003e4 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -574,12 +574,18 @@ drupal.node.preview:
 drupal.paragraphs.admin:
   css:
     component:
-      css/component/paragraphs-admin.css: { }
+      css/component/paragraphs.admin.css: { }
 
 drupal.paragraphs.widget:
   css:
     component:
-      css/component/paragraphs-widget.css: { }
+      css/component/paragraphs.widget.css: { }
+  js:
+    js/paragraphs.widget.js: {}
+  dependencies:
+    - core/drupal
+    - core/once
+    - dsfr4drupal/component.tab
 
 drupal.tabledrag:
   css:
@@ -600,8 +606,8 @@ element.horizontal_tabs:
   js:
     js/horizontal-tabs.js: {}
   dependencies:
-    - dsfr4drupal/component.tab
     - core/once
+    - dsfr4drupal/component.tab
 
 #legacy:
 #  js:
diff --git a/js/paragraphs.widget.js b/js/paragraphs.widget.js
new file mode 100644
index 0000000..bd349eb
--- /dev/null
+++ b/js/paragraphs.widget.js
@@ -0,0 +1,58 @@
+
+
+(function (Drupal, once) {
+  "use strict";
+
+  const CLASS_LIST = "fr-tabs__list";
+  const CLASS_TAB = "fr-tabs__tab";
+  const CLASS_TAB_SELECTED = "fr-tabs__tab--selected";
+  const CLASS_TABS = "fr-tabs";
+
+  Drupal.behaviors.dsfrParagraphsWidget = {
+    attach: (context) => {
+      const wrappers = once("dsfr-paragraphs-tabs-wrapper", ".paragraphs-tabs-wrapper", context);
+      if (!wrappers) {
+        return;
+      }
+
+      wrappers.forEach(wrapper => {
+        const list = wrapper.querySelector(".paragraphs-tabs");
+        if (
+          !list ||
+          list.classList.contains("paragraphs-tabs-hide")
+        ) {
+          return;
+        }
+
+        const listWrapper = document.createElement("div");
+        listWrapper.classList.add(CLASS_TABS);
+        wrapper.prepend(listWrapper);
+
+        listWrapper.appendChild(list);
+        list.classList.add(CLASS_LIST);
+
+        list.querySelectorAll("li > a").forEach(link => {
+          link.classList.add(CLASS_TAB);
+
+          if (link.classList.contains("is-active")) {
+            link.classList.add(CLASS_TAB_SELECTED);
+            link.setAttribute("aria-selected", "true");
+          }
+
+          link.addEventListener("click", () => {
+            // Remove selected class on old active link.
+            const selected = list.querySelector(`.${CLASS_TAB_SELECTED}`);
+            selected.classList.remove(CLASS_TAB_SELECTED);
+            selected.removeAttribute("aria-selected");
+
+            // Add class to new element.
+            link.classList.add(CLASS_TAB_SELECTED);
+            link.setAttribute("aria-selected", "true");
+          })
+        });
+      });
+
+    },
+  };
+
+})(Drupal, once);
-- 
GitLab


From a118f5fae5d66bdb519defca0385bec6e806bb1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Fri, 31 Jan 2025 16:24:01 +0100
Subject: [PATCH 19/45] Remove EOL.

---
 dsfr4drupal.theme | 1 -
 1 file changed, 1 deletion(-)

diff --git a/dsfr4drupal.theme b/dsfr4drupal.theme
index f2d9a31..0a99994 100644
--- a/dsfr4drupal.theme
+++ b/dsfr4drupal.theme
@@ -28,7 +28,6 @@ function dsfr4drupal_element_info_alter(array &$type): void {
   if (isset($type['vertical_tabs'])) {
     $type['vertical_tabs']['#pre_render'][] = [Dsfr4DrupalPreRender::class, 'verticalTabs'];
   }
-
 }
 
 /**
-- 
GitLab


From 96ca747015de02350a4e10fe04c1bc255cd90588 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 5 Feb 2025 11:20:32 +0100
Subject: [PATCH 20/45] Prepare to stylize Layout builder page.

---
 css/theme/layout-builder.css | 43 ++++++++++++++++++++++++++++++++++++
 dsfr4drupal.info.yml         |  2 ++
 dsfr4drupal.libraries.yml    |  5 +++++
 3 files changed, 50 insertions(+)
 create mode 100644 css/theme/layout-builder.css

diff --git a/css/theme/layout-builder.css b/css/theme/layout-builder.css
new file mode 100644
index 0000000..1f1274f
--- /dev/null
+++ b/css/theme/layout-builder.css
@@ -0,0 +1,43 @@
+/**
+ * @file
+ * Manage styles for layout builder.
+ */
+
+.layout-builder__link--add {
+  color: var(--text-default-grey);
+  padding-left: 0;
+}
+
+.layout-builder__link--add[href]:hover {
+  --underline-hover-width: 0;
+}
+
+.layout-builder__link--add::before {
+  content: '';
+  display: inline-block;
+  vertical-align: middle;
+  width: 16px;
+  height: 16px;
+  background-color: var(--text-default-grey);
+  mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16px' height='16px'%3e%3cpath d='M0.656,9.023c0,0.274,0.224,0.5,0.499,0.5l4.853,0.001c0.274-0.001,0.501,0.226,0.5,0.5l0.001,4.853 c-0.001,0.273,0.227,0.5,0.501,0.5l1.995-0.009c0.273-0.003,0.497-0.229,0.5-0.503l0.002-4.806c0-0.272,0.228-0.5,0.499-0.502 l4.831-0.021c0.271-0.005,0.497-0.23,0.501-0.502l0.008-1.998c0-0.276-0.225-0.5-0.499-0.5l-4.852,0c-0.275,0-0.502-0.228-0.501-0.5 L9.493,1.184c0-0.275-0.225-0.499-0.5-0.499L6.997,0.693C6.722,0.694,6.496,0.92,6.495,1.195L6.476,6.026 c-0.001,0.274-0.227,0.5-0.501,0.5L1.167,6.525C0.892,6.526,0.665,6.752,0.665,7.026L0.656,9.023z'/%3e%3c/svg%3e");
+  mask-position: left center;
+  mask-repeat: no-repeat;
+  mask-size: 100%;
+  margin-right: .25rem;
+}
+
+.layout-builder__link--add::before:hover {
+  background-color: var(--hover-tint);
+}
+
+.layout-builder__link--remove {
+  /* Force layout builder styles. */
+  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23bebebe' d='M3.51 13.925c.194.194.512.195.706.001l3.432-3.431c.194-.194.514-.194.708 0l3.432 3.431c.192.194.514.193.707-.001l1.405-1.417c.191-.195.189-.514-.002-.709l-3.397-3.4c-.192-.193-.192-.514-.002-.708l3.401-3.43c.189-.195.189-.515 0-.709l-1.407-1.418c-.195-.195-.513-.195-.707-.001l-3.43 3.431c-.195.194-.516.194-.708 0l-3.432-3.431c-.195-.195-.512-.194-.706.001l-1.407 1.417c-.194.195-.194.515 0 .71l3.403 3.429c.193.195.193.514-.001.708l-3.4 3.399c-.194.195-.195.516-.001.709l1.406 1.419z'/%3e%3c/svg%3e");
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-size: auto;
+}
+
+.layout-builder__region {
+  margin-top: .25rem;
+}
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index 3be50c0..ce69ee7 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -48,6 +48,8 @@ libraries-extend:
     - dsfr4drupal/drupal.vertical-tabs
   field_group/element.horizontal_tabs:
     - dsfr4drupal/element.horizontal_tabs
+  layout_builder/drupal.layout_builder:
+    - dsfr4drupal/drupal.layout_builder
   media_library/view:
     - dsfr4drupal/media_library.theme
   media_library/widget:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index 5f003e4..a081e62 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -560,6 +560,11 @@ drupal.form:
     component:
       css/component/form.css: {}
 
+drupal.layout_builder:
+  css:
+    theme:
+      css/theme/layout-builder.css: {}
+
 drupal.message:
   js:
     js/messages.js: {}
-- 
GitLab


From bd3a51e6763c0dec16287b6c4f604a6994b6b9f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Thu, 6 Feb 2025 11:56:29 +0100
Subject: [PATCH 21/45] Finalize to stylize Layout builder page.

---
 includes/form.theme                           | 23 +----------
 includes/system.theme                         | 38 +++++++++++++++++++
 .../details--dialog-off-canvas.html.twig      |  2 +
 3 files changed, 41 insertions(+), 22 deletions(-)
 create mode 100644 includes/system.theme
 create mode 100644 templates/system/details--dialog-off-canvas.html.twig

diff --git a/includes/form.theme b/includes/form.theme
index 02e5c9c..74935ab 100644
--- a/includes/form.theme
+++ b/includes/form.theme
@@ -278,28 +278,7 @@ function dsfr4drupal_preprocess_textarea(array &$variables): void {
 }
 
 /**
- * Implements hook_theme_suggestions_HOOK_alter() for details.
- */
-function dsfr4drupal_theme_suggestions_details_alter(array &$suggestions, array $variables): void {
-  /**
-   * The property "#horizontal_tab_item" is added during element prerender.
-   * @see: \Drupal\dsfr4drupal\Dsfr4DrupalPrerender::horizontalTabs()
-   */
-  if (!empty($variables['element']['#horizontal_tab_item'])) {
-    $suggestions[] = 'details__horizontal_tabs';
-  }
-
-  /**
-   * The property "#vertical_tab_item" is added during element prerender.
-   * @see: \Drupal\dsfr4drupal\Dsfr4DrupalPrerender::verticalTabs()
-   */
-  if (!empty($variables['element']['#vertical_tab_item'])) {
-    $suggestions[] = 'details__vertical_tabs';
-  }
-}
-
-/**
- * Implements hook_theme_suggestions_HOOK_alter() for "form_elemtn".
+ * Implements hook_theme_suggestions_HOOK_alter() for "form_element".
  */
 function dsfr4drupal_theme_suggestions_form_element_alter(array &$suggestions, array $variables): void {
   if (empty($suggestions)) {
diff --git a/includes/system.theme b/includes/system.theme
new file mode 100644
index 0000000..49f3b39
--- /dev/null
+++ b/includes/system.theme
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Functions to support system theming in the "DSFR for Drupal" theme.
+ */
+
+declare(strict_types=1);
+
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter() for details.
+ */
+function dsfr4drupal_theme_suggestions_details_alter(array &$suggestions, array $variables): void {
+  /**
+   * The property "#horizontal_tab_item" is added during element prerender.
+   * @see: \Drupal\dsfr4drupal\Dsfr4DrupalPrerender::horizontalTabs()
+   */
+  if (!empty($variables['element']['#horizontal_tab_item'])) {
+    $suggestions[] = 'details__horizontal_tabs';
+  }
+
+  /**
+   * The property "#vertical_tab_item" is added during element prerender.
+   * @see: \Drupal\dsfr4drupal\Dsfr4DrupalPrerender::verticalTabs()
+   */
+  if (!empty($variables['element']['#vertical_tab_item'])) {
+    $suggestions[] = 'details__vertical_tabs';
+  }
+
+  /**
+   * Styles are reset with Drupal dialog off canvas.
+   * Use default details rendering instead of DSFR accordion component.
+   */
+  if (\Drupal::request()->query->get('_wrapper_format') === 'drupal_dialog.off_canvas') {
+    $suggestions[] = 'details__dialog_off_canvas';
+  }
+}
diff --git a/templates/system/details--dialog-off-canvas.html.twig b/templates/system/details--dialog-off-canvas.html.twig
new file mode 100644
index 0000000..9bbbd49
--- /dev/null
+++ b/templates/system/details--dialog-off-canvas.html.twig
@@ -0,0 +1,2 @@
+{# Styles are reset with Drupal dialog off canvas. Use default details rendering instead of DSFR accordion component. #}
+{% include '@system/templates/details.html.twig' %}
-- 
GitLab


From 3e545f7b1c93c03ac632b6feaa3332a06095dc64 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Thu, 6 Feb 2025 17:25:15 +0100
Subject: [PATCH 22/45] Remove double closing div.

---
 templates/form/input--password.html.twig | 1 -
 1 file changed, 1 deletion(-)

diff --git a/templates/form/input--password.html.twig b/templates/form/input--password.html.twig
index 0fb51b5..93718b4 100644
--- a/templates/form/input--password.html.twig
+++ b/templates/form/input--password.html.twig
@@ -13,7 +13,6 @@
     <label class="fr--password__checkbox fr-label" for="{{ show_password_id }}">
       {{ show_password_label }}
     </label>
-    </div>
   </div>
 {% endif %}
 {{ attach_library('dsfr4drupal/component.password') }}
-- 
GitLab


From be52ebce3b52c99fe391bd87b4b2bc8bd5fed703 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Mon, 17 Feb 2025 15:57:46 +0100
Subject: [PATCH 23/45] Manage Select2 widget styles.

---
 css/theme/select2.css     | 140 ++++++++++++++++++++++++++++++++++++++
 dsfr4drupal.info.yml      |   2 +
 dsfr4drupal.libraries.yml |   5 ++
 3 files changed, 147 insertions(+)
 create mode 100644 css/theme/select2.css

diff --git a/css/theme/select2.css b/css/theme/select2.css
new file mode 100644
index 0000000..24d8b4d
--- /dev/null
+++ b/css/theme/select2.css
@@ -0,0 +1,140 @@
+/**
+ * @file
+ * Manage styles for Select2 widget.
+ */
+
+/* Duplicate "select" component styles - START */
+.select2-container .selection {
+  display: block;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  width: 100%;
+  border-radius: 0.25rem 0.25rem 0 0;
+  font-size: 1rem;
+  line-height: 1.5rem;
+  padding: 0.5rem 2.5rem 0.5rem 1rem;
+  background-repeat: no-repeat;
+  background-position: calc(100% - 1rem) 50%;
+  background-size: 1rem 1rem;
+  color: var(--text-default-grey);
+  background-color: var(--background-contrast-grey);
+
+  --idle: transparent;
+  --hover: var(--background-contrast-grey-hover);
+  --active: var(--background-contrast-grey-active);
+  box-shadow: inset 0 -2px 0 0 var(--border-plain-grey);
+
+  --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23161616' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
+  background-image: var(--data-uri-svg);
+}
+
+:root[data-fr-theme=dark] .select2-container {
+  --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23fff' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
+}
+
+.fr-fieldset--error .select2-container,
+.fr-select-group--error .select2-container {
+  box-shadow: inset 0 -2px 0 0 var(--border-plain-error);
+}
+
+.fr-select:disabled + .select2-container {
+  color: var(--text-disabled-grey);
+  box-shadow: inset 0 -2px 0 0 var(--border-disabled-grey);
+
+  --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23929292' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
+  background-image: var(--data-uri-svg);
+}
+
+:root[data-fr-theme=dark] .fr-select:disabled + .select2-container {
+  --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23666' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
+  cursor: not-allowed;
+}
+
+.select2-container:-webkit-autofill,
+.select2-container:-webkit-autofill:hover,
+.select2-container:-webkit-autofill:focus {
+  box-shadow: inset 0 -2px 0 0 var(--border-plain-grey), inset 0 0 0 1000px var(--background-contrast-blue-france);
+  -webkit-text-fill-color: var(--text-label-grey);
+}
+
+@media (-ms-high-contrast: active), (forced-colors: active) {
+  .select2-container {
+    background-image: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' fill='canvastext'><path d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
+  }
+}
+/* Duplicate "select" component styles - END */
+
+.select2-container .select2-selection--multiple {
+  background-color: transparent;
+  padding: 0;
+  display: block;
+}
+
+.select2-container .select2-selection--multiple {
+  min-height: auto;
+}
+
+.select2-container .select2-selection--multiple,
+.select2-container.select2-container--focus .select2-selection--multiple {
+  border: 0;
+}
+
+.select2-container .select2-search {
+  display: inline-block;
+}
+
+.select2-container .select2-search--inline .select2-search__field {
+  margin: 0;
+  font-size: inherit;
+  font-family: "Marianne", arial, sans-serif;
+  height: 24px;
+}
+
+.select2-container .select2-search--inline .select2-search__field::placeholder {
+  color: var(--text-default-grey);
+}
+
+.select2-container .select2-selection--single .select2-selection__placeholder {
+  color: var(--text-mention-grey);
+}
+
+.select2-dropdown {
+  margin-top: 2px;
+  padding: .25rem;
+  background-color: var(--background-contrast-grey);
+}
+
+.select2-container--open .select2-dropdown--below {
+  border: 1px solid var(--border-plain-grey);
+}
+
+.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable {
+  background-color: var(--background-contrast-grey-hover);
+  color: var(--text-default-grey);
+}
+
+.select2-selection__rendered .select2-selection__choice {
+  margin-right: .5rem;
+}
+
+.select2-container--default .select2-selection--multiple .select2-selection__choice {
+  background-color: var(--background-action-low-blue-france);
+  color: var(--text-action-high-blue-france);
+  border-radius: 1rem;
+  font-size: .875rem;
+  padding: .25rem .75rem .25rem 1.25rem;
+  border: 0;
+}
+.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
+  border: 0;
+  color: var(--text-action-high-blue-france);
+  font-size: 1.5em;
+  padding: 0 .25rem;
+  left: .2rem;
+}
+.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,
+.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus {
+  background-color: transparent;
+  color: inherit;
+}
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index ce69ee7..a563b75 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -62,6 +62,8 @@ libraries-extend:
     - dsfr4drupal/drupal.paragraphs.admin
   paragraphs/drupal.paragraphs.widget:
     - dsfr4drupal/drupal.paragraphs.widget
+  select2/select2:
+    - dsfr4drupal/select2
   tarte_au_citron/tarte_au_citron_lib:
     - dsfr4drupal/tarteaucitron
   tacjs/tarteaucitron.js:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index a081e62..05e20d9 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -649,6 +649,11 @@ scheme:
   dependencies:
     - dsfr4drupal/core
 
+select2:
+  css:
+    theme:
+      css/theme/select2.css: {}
+
 tarteaucitron:
   css:
     theme:
-- 
GitLab


From 50defcd43ddf19511a530685f088fb6f22e20c9d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Mon, 17 Feb 2025 16:06:46 +0100
Subject: [PATCH 24/45] Fix error when field has not description.

---
 includes/form.theme | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/includes/form.theme b/includes/form.theme
index 74935ab..22707eb 100644
--- a/includes/form.theme
+++ b/includes/form.theme
@@ -146,7 +146,7 @@ function dsfr4drupal_preprocess_form_element(array &$variables): void {
  */
 function dsfr4drupal_preprocess_form_element_label(array &$variables): void {
   // We need to have description in form element label template.
-  $variables['description'] = $variables['element']['#description'];
+  $variables['description'] = $variables['element']['#description'] ?? '';
 
   // Show help text as tooltip.
   $variables['description_tooltip'] = theme_get_setting('form_description_tooltip') ?? FALSE;
-- 
GitLab


From 3d674c8f0ee7284573954cf855f9e4f3c8984291 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Mon, 17 Feb 2025 16:25:48 +0100
Subject: [PATCH 25/45] Fix dismiss rendering.

---
 css/theme/select2.css | 42 ++++++++++++++++++++++++++++++++++++------
 1 file changed, 36 insertions(+), 6 deletions(-)

diff --git a/css/theme/select2.css b/css/theme/select2.css
index 24d8b4d..24754b6 100644
--- a/css/theme/select2.css
+++ b/css/theme/select2.css
@@ -119,22 +119,52 @@
 }
 
 .select2-container--default .select2-selection--multiple .select2-selection__choice {
-  background-color: var(--background-action-low-blue-france);
-  color: var(--text-action-high-blue-france);
+  position: relative;
+  display: inline-flex;
+  background-color: var(--background-action-high-blue-france);
+  color: var(--text-inverted-blue-france);
   border-radius: 1rem;
   font-size: .875rem;
-  padding: .25rem .75rem .25rem 1.25rem;
+  padding: .25rem .75rem;
   border: 0;
 }
+
+.select2-container--default .select2-selection--multiple .select2-selection__choice:hover {
+  background-color: var(--background-action-high-blue-france-hover);
+}
+
 .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
+  position: initial;
+  left: auto;
+  top: auto;
+  display: flex;
+  order: 2;
+  padding: 0 .25rem;
   border: 0;
-  color: var(--text-action-high-blue-france);
   font-size: 1.5em;
-  padding: 0 .25rem;
-  left: .2rem;
+  line-height: 1;
+  color: var(--text-inverted-blue-france);
+}
+
+.select2-container--default .select2-selection--multiple .select2-selection__choice__remove::before {
+  content: "";
+  position: absolute;
+  display: block;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 1;
 }
+
 .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,
 .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus {
   background-color: transparent;
   color: inherit;
 }
+
+.select2-container--default .select2-selection--multiple .select2-selection__choice__display {
+  display: flex;
+}
-- 
GitLab


From 0dd237f7f4faf2152e76e98d16e3323da62c6ac2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Tue, 25 Feb 2025 13:29:37 +0100
Subject: [PATCH 26/45] Fix accordion in horizontal/vertical tabs.

---
 css/theme/horizontal-tabs.css | 1 +
 css/theme/vertical-tabs.css   | 1 +
 2 files changed, 2 insertions(+)

diff --git a/css/theme/horizontal-tabs.css b/css/theme/horizontal-tabs.css
index f0d6610..191b5f3 100644
--- a/css/theme/horizontal-tabs.css
+++ b/css/theme/horizontal-tabs.css
@@ -56,6 +56,7 @@
   padding: 0;
 }
 
+.horizontal-tabs-panes .fr-accordion__btn,
 .horizontal-tabs-panes .fr-input,
 .horizontal-tabs-panes .fr-select {
   box-sizing: border-box;
diff --git a/css/theme/vertical-tabs.css b/css/theme/vertical-tabs.css
index 43ae05c..120a651 100644
--- a/css/theme/vertical-tabs.css
+++ b/css/theme/vertical-tabs.css
@@ -44,6 +44,7 @@
   padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing-s);
 }
 
+.vertical-tabs__pane .fr-accordion__btn,
 .vertical-tabs__pane .fr-input,
 .vertical-tabs__pane .fr-select {
   box-sizing: border-box;
-- 
GitLab


From 79cd12b29a3c8738e4a2b70c8b7a366e09df0ab8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Tue, 4 Mar 2025 15:44:08 +0100
Subject: [PATCH 27/45] Move core.css to base directory.

---
 css/{ => base}/core.css   | 0
 dsfr4drupal.libraries.yml | 2 +-
 2 files changed, 1 insertion(+), 1 deletion(-)
 rename css/{ => base}/core.css (100%)

diff --git a/css/core.css b/css/base/core.css
similarity index 100%
rename from css/core.css
rename to css/base/core.css
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index be6ca7d..590ba7a 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -527,7 +527,7 @@ core:
   css:
     base:
       /libraries/dsfr/dist/core/core.min.css: { minified: true }
-      css/core.css: {}
+      css/base/core.css: {}
   js:
     js/core.js: {}
     /libraries/dsfr/dist/core/core.module.min.js:
-- 
GitLab


From 30d6dee9b1e6600dfd2db63035dc6f7394709a76 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Tue, 4 Mar 2025 16:02:33 +0100
Subject: [PATCH 28/45] Reduce filter text size.

---
 css/component/form.css | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/css/component/form.css b/css/component/form.css
index f612baf..d10c493 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -18,14 +18,12 @@
 /* Provide a class to remove spacing. */
 .dsfr4drupal-form--no-spacing {
   --form-spacing: 0;
-  --form-item-spacing: 0;
 }
 
 /* Unset form spacing for default DSFR forms. */
 .fr-follow__newsletter,
 .fr-search-bar {
   --form-spacing: 0;
-  --form-item-spacing: 0;
 }
 
 main[role="main"] > .fr-container > form,
@@ -33,10 +31,22 @@ main[role="main"] > .fr-container--fluid > form {
   margin-block: var(--form-spacing);
 }
 
+.fr-label > .field-edit-link {
+  font-size: .75em;
+}
+
+.fr-label .field-edit-link > button {
+  padding: 0;
+}
+
 .form-wrapper {
   margin-block: var(--form-item-spacing);
 }
 
+.form-wrapper.js-filter-wrapper {
+  font-size: .75em;
+}
+
 .form-actions a + button,
 .form-actions button + button,
 .form-actions button + a {
-- 
GitLab


From cc23ce00c85179acbcf8ab2e2e74f9a9290f8c93 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 5 Mar 2025 12:06:34 +0100
Subject: [PATCH 29/45] Fix feedbacks.

---
 includes/media.theme                          | 18 -------------
 js/paragraphs.widget.js                       |  5 +++-
 templates/block/block.html.twig               | 20 +++++++-------
 .../media/media--media-library.html.twig      | 26 +++++++++----------
 templates/media/media.html.twig               |  8 +++---
 templates/node/node.html.twig                 | 14 +++++-----
 templates/region/region.html.twig             |  6 ++---
 .../views/views-view--media-library.html.twig |  8 +++---
 8 files changed, 44 insertions(+), 61 deletions(-)
 delete mode 100644 includes/media.theme

diff --git a/includes/media.theme b/includes/media.theme
deleted file mode 100644
index d98dfcb..0000000
--- a/includes/media.theme
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-/**
- * @file
- * Functions to support media theming in the "DSFR for Drupal" theme.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_preprocss_hook() for "media__media_library".
- */
-function dsfr4drupal_preprocess_media__media_library(array &$variables): void {
-  /** @var \Drupal\media\MediaInterface $media */
-  $media = $variables['media'];
-
-
-}
diff --git a/js/paragraphs.widget.js b/js/paragraphs.widget.js
index bd349eb..786664b 100644
--- a/js/paragraphs.widget.js
+++ b/js/paragraphs.widget.js
@@ -1,4 +1,7 @@
-
+/**
+ * @file
+ * Manage paragraphs widget rendering with DSFR styles.
+ */
 
 (function (Drupal, once) {
   "use strict";
diff --git a/templates/block/block.html.twig b/templates/block/block.html.twig
index 1ac5205..221b492 100644
--- a/templates/block/block.html.twig
+++ b/templates/block/block.html.twig
@@ -1,16 +1,16 @@
 {% if attributes is not empty %}
-<div{{ attributes }}>
+  <div{{ attributes }}>
 {% endif %}
 
-  {{ title_prefix }}
-  {% if label %}
-    <h2{{ title_attributes }}>{{ label }}</h2>
-  {% endif %}
-  {{ title_suffix }}
-  {% block content %}
-    {{ content }}
-  {% endblock %}
+{{ title_prefix }}
+{% if label %}
+  <h2{{ title_attributes }}>{{ label }}</h2>
+{% endif %}
+{{ title_suffix }}
+{% block content %}
+  {{ content }}
+{% endblock %}
 
 {% if attributes is not empty %}
-</div>
+  </div>
 {% endif %}
diff --git a/templates/media/media--media-library.html.twig b/templates/media/media--media-library.html.twig
index 9f70bd2..e51ace7 100644
--- a/templates/media/media--media-library.html.twig
+++ b/templates/media/media--media-library.html.twig
@@ -1,19 +1,19 @@
 {% if attributes is not empty %}
-<div{{ attributes }}>
+  <div{{ attributes }}>
 {% endif %}
 
-  <div{{ preview_attributes.addClass('js-media-library-item-preview') }}>
-    {{ include('dsfr4drupal:card', {
-      'image': content.thumbnail ? content.thumbnail : '',
-      'title': name,
-      'title_attributes': metadata_attributes,
-      'description': content|without('thumbnail'),
-      'no_arrow': true,
-      'detail': not status ? 'unpublished'|t : '',
-      'detail_icon': 'fr-icon-warning-fill',
-      'variant': 'sm',
-    }, with_context=false) }}
-  </div>
+<div{{ preview_attributes.addClass('js-media-library-item-preview') }}>
+  {{ include('dsfr4drupal:card', {
+    'image': content.thumbnail ? content.thumbnail : '',
+    'title': name,
+    'title_attributes': metadata_attributes,
+    'description': content|without('thumbnail'),
+    'no_arrow': true,
+    'detail': not status ? 'unpublished'|t : '',
+    'detail_icon': 'fr-icon-warning-fill',
+    'variant': 'sm',
+  }, with_context=false) }}
+</div>
 
 {% if attributes is not empty %}
   </div>
diff --git a/templates/media/media.html.twig b/templates/media/media.html.twig
index 427b3f5..349904d 100644
--- a/templates/media/media.html.twig
+++ b/templates/media/media.html.twig
@@ -1,10 +1,10 @@
 {% if attributes is not empty %}
-<div{{ attributes }}>
+  <div{{ attributes }}>
 {% endif %}
 
- {{ title_suffix.contextual_links }}
- {{ content }}
+{{ title_suffix.contextual_links }}
+{{ content }}
 
 {% if attributes is not empty %}
-</div>
+  </div>
 {% endif %}
diff --git a/templates/node/node.html.twig b/templates/node/node.html.twig
index b23d6da..f46d52d 100644
--- a/templates/node/node.html.twig
+++ b/templates/node/node.html.twig
@@ -1,17 +1,17 @@
 {% if attributes is not empty %}
-<div{{ attributes }}>
+  <div{{ attributes }}>
 {% endif %}
 
-  {% if content_attributes is not empty %}
+{% if content_attributes is not empty %}
   <div{{ content_attributes }}>
-  {% endif %}
+{% endif %}
 
-    {{ content }}
+{{ content }}
 
-  {% if content_attributes is not empty %}
+{% if content_attributes is not empty %}
   </div>
-  {% endif %}
+{% endif %}
 
 {% if attributes is not empty %}
-</div>
+  </div>
 {% endif %}
diff --git a/templates/region/region.html.twig b/templates/region/region.html.twig
index ce19558..80f6ad3 100644
--- a/templates/region/region.html.twig
+++ b/templates/region/region.html.twig
@@ -1,11 +1,11 @@
 {% if content %}
   {% if attributes is not empty %}
-  <div{{ attributes }}>
+    <div{{ attributes }}>
   {% endif %}
 
-    {{ content }}
+  {{ content }}
 
   {% if attributes is not empty %}
-  </div>
+    </div>
   {% endif %}
 {% endif %}
diff --git a/templates/views/views-view--media-library.html.twig b/templates/views/views-view--media-library.html.twig
index 0b66c96..d31d2c2 100644
--- a/templates/views/views-view--media-library.html.twig
+++ b/templates/views/views-view--media-library.html.twig
@@ -1,9 +1,7 @@
-
-{%
-  set classes = [
+{% set classes = [
   dom_id ? 'js-view-dom-id-' ~ dom_id,
-]
-%}
+] %}
+
 <div{{ attributes.addClass(classes) }}>
   {{ title_prefix }}
   {{ title }}
-- 
GitLab


From 5794d54c57b2a8401725f444ba50e50b6879ac60 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 5 Mar 2025 12:14:58 +0100
Subject: [PATCH 30/45] Fix feedbacks.

---
 includes/form.theme   | 4 ++--
 includes/system.theme | 1 -
 js/accordion.js       | 1 +
 js/horizontal-tabs.js | 1 +
 js/tabledrag.js       | 5 +++--
 5 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/includes/form.theme b/includes/form.theme
index 53aefc0..db0eeb8 100644
--- a/includes/form.theme
+++ b/includes/form.theme
@@ -73,7 +73,7 @@ function dsfr4drupal_preprocess_form(array &$variables): void {
 /**
  * Implements hook_preprocess_HOOK() for "views_exposed_form".
  */
-function dsfr4drupal_preprocess_views_exposed_form(&$variables) {
+function dsfr4drupal_preprocess_views_exposed_form(array &$variables): void {
   $form = &$variables['form'];
 
   // Add BEM classes for items in the form.
@@ -81,7 +81,7 @@ function dsfr4drupal_preprocess_views_exposed_form(&$variables) {
   foreach (Element::children($form, TRUE) as $child_key) {
     if (!empty($form[$child_key]['#type'])) {
       if ($form[$child_key]['#type'] === 'actions') {
-        // We need the key of the element that precedes the actions element.
+        // We need the key of the element that precedes the actions' element.
         $form[$child_key]['#attributes']['class'][] = 'views-exposed-form__item';
         $form[$child_key]['#attributes']['class'][] = 'views-exposed-form__item--actions';
       }
diff --git a/includes/system.theme b/includes/system.theme
index 49f3b39..7b3bf79 100644
--- a/includes/system.theme
+++ b/includes/system.theme
@@ -7,7 +7,6 @@
 
 declare(strict_types=1);
 
-
 /**
  * Implements hook_theme_suggestions_HOOK_alter() for details.
  */
diff --git a/js/accordion.js b/js/accordion.js
index 14679c1..e2202d9 100644
--- a/js/accordion.js
+++ b/js/accordion.js
@@ -4,6 +4,7 @@
  */
 
 ((Drupal, once) => {
+  "use strict";
 
   Drupal.behaviors.dsfrAccordion = {
     attach: (context) => {
diff --git a/js/horizontal-tabs.js b/js/horizontal-tabs.js
index cb1604a..a58d335 100644
--- a/js/horizontal-tabs.js
+++ b/js/horizontal-tabs.js
@@ -4,6 +4,7 @@
  */
 
 ((Drupal, once) => {
+  "use strict";
 
   const CLASS_LIST = "fr-tabs__list";
   const CLASS_TAB = "fr-tabs__tab";
diff --git a/js/tabledrag.js b/js/tabledrag.js
index da730da..1c3db8c 100644
--- a/js/tabledrag.js
+++ b/js/tabledrag.js
@@ -10,6 +10,7 @@
  */
 
 (function ($, Drupal, drupalSettings) {
+  "use strict";
 
   /**
    * Store the state of weight columns display for all tables.
@@ -24,7 +25,7 @@
   const addChangedWarningOriginal = Drupal.tableDrag.prototype.row.prototype.addChangedWarning;
 
   /**
-   @inheritDoc
+   * {@inheritdoc}
    */
   Drupal.tableDrag.prototype.row.prototype.addChangedWarning = function () {
     const $table = $(this.table.parentNode);
@@ -41,7 +42,7 @@
   };
 
   /**
-   * @inheritDoc
+   * {@inheritdoc}
    */
   Drupal.tableDrag.prototype.initColumns = function () {
     const $tableWrapper = this.$table.parents(".fr-table");
-- 
GitLab


From c48cfd677155caefea8b2a0c84ffe527588e0484 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 5 Mar 2025 14:19:53 +0100
Subject: [PATCH 31/45] Fix feedbacks.

---
 css/ckeditor5.css             | 27 +++++++++++++++++++++-
 css/component/checkbox.css    |  1 +
 css/component/form.css        | 16 ++++++-------
 css/theme/horizontal-tabs.css | 13 +++++------
 css/theme/media-library.css   |  2 +-
 css/theme/select2.css         | 43 ++++++++++++++++-------------------
 css/theme/ui-dialog.css       | 13 +++++++----
 css/theme/vertical-tabs.css   | 14 ++++++------
 dsfr4drupal.libraries.yml     |  3 +++
 js/horizontal-tabs.js         |  7 +-----
 js/paragraphs.widget.js       |  1 -
 js/tabledrag.js               | 36 ++++++++++++-----------------
 12 files changed, 94 insertions(+), 82 deletions(-)

diff --git a/css/ckeditor5.css b/css/ckeditor5.css
index a1ca5da..94e00f6 100644
--- a/css/ckeditor5.css
+++ b/css/ckeditor5.css
@@ -111,7 +111,7 @@
   --underline-thickness: 0.0625em;
   --underline-img: linear-gradient(0deg, currentcolor, currentcolor);
   --xl-base: 1em;
-  --ol-content: counters(li-counter, ".") ".  ";
+  --ol-content: counters(li-counter, ".") ".  "; /* stylelint-disable-line no-irregular-whitespace */
 
   color: var(--text-default-grey);
   font-family: Marianne,arial,sans-serif;
@@ -252,78 +252,103 @@
 .ck-content ol[start] {
   counter-reset: none;
 }
+
 .ck-content ol[start="0"] {
   counter-reset: li-counter -1;
 }
+
 .ck-content ol[start="2"] {
   counter-reset: li-counter 1;
 }
+
 .ck-content ol[start="3"] {
   counter-reset: li-counter 2;
 }
+
 .ck-content ol[start="4"] {
   counter-reset: li-counter 3;
 }
+
 .ck-content ol[start="5"] {
   counter-reset: li-counter 4;
 }
+
 .ck-content ol[start="6"] {
   counter-reset: li-counter 5;
 }
+
 .ck-content ol[start="7"] {
   counter-reset: li-counter 6;
 }
+
 .ck-content ol[start="8"] {
   counter-reset: li-counter 7;
 }
+
 .ck-content ol[start="9"] {
   counter-reset: li-counter 8;
 }
+
 .ck-content ol[start="10"] {
   counter-reset: li-counter 9;
 }
+
 .ck-content ol[start="11"] {
   counter-reset: li-counter 10;
 }
+
 .ck-content ol[start="12"] {
   counter-reset: li-counter 11;
 }
+
 .ck-content ol[start="13"] {
   counter-reset: li-counter 12;
 }
+
 .ck-content ol[start="14"] {
   counter-reset: li-counter 13;
 }
+
 .ck-content ol[start="15"] {
   counter-reset: li-counter 14;
 }
+
 .ck-content ol[start="16"] {
   counter-reset: li-counter 15;
 }
+
 .ck-content ol[start="17"] {
   counter-reset: li-counter 16;
 }
+
 .ck-content ol[start="18"] {
   counter-reset: li-counter 17;
 }
+
 .ck-content ol[start="19"] {
   counter-reset: li-counter 18;
 }
+
 .ck-content ol[start="20"] {
   counter-reset: li-counter 19;
 }
+
 .ck-content ol[start="21"] {
   counter-reset: li-counter 20;
 }
+
 .ck-content ol[start="22"] {
   counter-reset: li-counter 21;
 }
+
 .ck-content ol[start="23"] {
   counter-reset: li-counter 22;
 }
+
 .ck-content ol[start="24"] {
   counter-reset: li-counter 23;
 }
+
 .ck-content ol[start="25"] {
   counter-reset: li-counter 24;
 }
diff --git a/css/component/checkbox.css b/css/component/checkbox.css
index f41a5f5..412003e 100644
--- a/css/component/checkbox.css
+++ b/css/component/checkbox.css
@@ -13,6 +13,7 @@
   clip: auto;
   z-index: 1;
 }
+
 .fr-checkbox-group input[type="checkbox"] + label.visually-hidden::before {
   left: 0;
 }
diff --git a/css/component/form.css b/css/component/form.css
index d10c493..4dc6fb9 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -31,14 +31,6 @@ main[role="main"] > .fr-container--fluid > form {
   margin-block: var(--form-spacing);
 }
 
-.fr-label > .field-edit-link {
-  font-size: .75em;
-}
-
-.fr-label .field-edit-link > button {
-  padding: 0;
-}
-
 .form-wrapper {
   margin-block: var(--form-item-spacing);
 }
@@ -76,6 +68,14 @@ main[role="main"] > .fr-container--fluid > form {
   border: 1px solid var(--border-default-grey);
 }
 
+.fr-label > .field-edit-link {
+  font-size: .75em;
+}
+
+.fr-label .field-edit-link > button {
+  padding: 0;
+}
+
 .fr-upload-group {
   padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing);
   border: 1px solid var(--border-default-grey);
diff --git a/css/theme/horizontal-tabs.css b/css/theme/horizontal-tabs.css
index 191b5f3..6943d5d 100644
--- a/css/theme/horizontal-tabs.css
+++ b/css/theme/horizontal-tabs.css
@@ -20,8 +20,7 @@
   padding: 4px .75rem;
 }
 
-.horizontal-tabs ul.horizontal-tabs-list li a,
-.horizontal-tabs ul.horizontal-tabs-list li.selected a {
+.horizontal-tabs ul.horizontal-tabs-list li a {
   /* Force DSFR tabs styles */
   display: inline-flex;
   padding: .5rem 1rem;
@@ -41,11 +40,6 @@
   background-color: var(--hover-tint);
 }
 
-.horizontal-tabs-list.fr-tabs__list + .horizontal-tabs-panes {
-  /* Fix order */
-  order: 3;
-}
-
 .horizontal-tabs-panes {
   border: 1px solid var(--border-default-grey);
   border-top: 0;
@@ -56,6 +50,11 @@
   padding: 0;
 }
 
+.horizontal-tabs-list.fr-tabs__list + .horizontal-tabs-panes {
+  /* Fix order */
+  order: 3;
+}
+
 .horizontal-tabs-panes .fr-accordion__btn,
 .horizontal-tabs-panes .fr-input,
 .horizontal-tabs-panes .fr-select {
diff --git a/css/theme/media-library.css b/css/theme/media-library.css
index e1bddc1..e5d4c7b 100644
--- a/css/theme/media-library.css
+++ b/css/theme/media-library.css
@@ -13,7 +13,7 @@
 }
 
 .js-media-library-item {
-  margin-block: var(--form-item-spacing) !important;
+  margin-block: var(--form-item-spacing) !important; /* stylelint-disable-line declaration-no-important */
 }
 
 .js-media-library-add-form-added-media {
diff --git a/css/theme/select2.css b/css/theme/select2.css
index 24754b6..7552863 100644
--- a/css/theme/select2.css
+++ b/css/theme/select2.css
@@ -6,8 +6,6 @@
 /* Duplicate "select" component styles - START */
 .select2-container .selection {
   display: block;
-  -webkit-appearance: none;
-  -moz-appearance: none;
   appearance: none;
   width: 100%;
   border-radius: 0.25rem 0.25rem 0 0;
@@ -23,14 +21,12 @@
   --idle: transparent;
   --hover: var(--background-contrast-grey-hover);
   --active: var(--background-contrast-grey-active);
+
   box-shadow: inset 0 -2px 0 0 var(--border-plain-grey);
 
   --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23161616' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
-  background-image: var(--data-uri-svg);
-}
 
-:root[data-fr-theme=dark] .select2-container {
-  --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23fff' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
+  background-image: var(--data-uri-svg);
 }
 
 .fr-fieldset--error .select2-container,
@@ -38,24 +34,29 @@
   box-shadow: inset 0 -2px 0 0 var(--border-plain-error);
 }
 
+.select2-container:-webkit-autofill,
+.select2-container:-webkit-autofill:hover,
+.select2-container:-webkit-autofill:focus {
+  box-shadow: inset 0 -2px 0 0 var(--border-plain-grey), inset 0 0 0 1000px var(--background-contrast-blue-france);
+  -webkit-text-fill-color: var(--text-label-grey);
+}
+
 .fr-select:disabled + .select2-container {
+  --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23929292' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
+
   color: var(--text-disabled-grey);
   box-shadow: inset 0 -2px 0 0 var(--border-disabled-grey);
-
-  --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23929292' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
   background-image: var(--data-uri-svg);
 }
 
-:root[data-fr-theme=dark] .fr-select:disabled + .select2-container {
-  --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23666' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
-  cursor: not-allowed;
+:root[data-fr-theme="dark"] .select2-container {
+  --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23fff' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
 }
 
-.select2-container:-webkit-autofill,
-.select2-container:-webkit-autofill:hover,
-.select2-container:-webkit-autofill:focus {
-  box-shadow: inset 0 -2px 0 0 var(--border-plain-grey), inset 0 0 0 1000px var(--background-contrast-blue-france);
-  -webkit-text-fill-color: var(--text-label-grey);
+:root[data-fr-theme="dark"] .fr-select:disabled + .select2-container {
+  --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23666' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>");
+
+  cursor: not-allowed;
 }
 
 @media (-ms-high-contrast: active), (forced-colors: active) {
@@ -69,9 +70,6 @@
   background-color: transparent;
   padding: 0;
   display: block;
-}
-
-.select2-container .select2-selection--multiple {
   min-height: auto;
 }
 
@@ -87,7 +85,7 @@
 .select2-container .select2-search--inline .select2-search__field {
   margin: 0;
   font-size: inherit;
-  font-family: "Marianne", arial, sans-serif;
+  font-family: Marianne, arial, sans-serif;
   height: 24px;
 }
 
@@ -150,10 +148,7 @@
   content: "";
   position: absolute;
   display: block;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  top: 0;
+  inset: 0;
   width: 100%;
   height: 100%;
   z-index: 1;
diff --git a/css/theme/ui-dialog.css b/css/theme/ui-dialog.css
index f8e44b7..a682040 100644
--- a/css/theme/ui-dialog.css
+++ b/css/theme/ui-dialog.css
@@ -5,7 +5,8 @@
  */
 
 .ui-dialog {
-  padding: 0;  max-height: 80vh !important;
+  padding: 0;
+  max-height: 80vh !important; /* stylelint-disable-line declaration-no-important */
   filter: drop-shadow(var(--lifted-shadow));
 }
 
@@ -29,6 +30,7 @@
   background: var(--background-default-grey);
   color: var(--text-default-grey);
 }
+
 .ui-widget.ui-widget-content {
   border: 0;
 }
@@ -41,7 +43,7 @@
   font-weight: 700;
 }
 
-@media (min-width: 48em) {
+@media (width >= 48em) {
   .ui-widget-header {
     font-size: 1.5rem;
     line-height: 2rem;
@@ -66,6 +68,7 @@
   line-height: 1.5rem;
   overflow: initial;
 }
+
 .ui-dialog .ui-dialog-titlebar-close:hover {
   background-color: var(--hover-tint);
 }
@@ -73,16 +76,16 @@
 .ui-dialog .ui-dialog-titlebar-close::after {
   --icon-size: 1rem;
 
-  background-color: currentColor;
+  background-color: currentcolor;
   content: '';
   display: inline-block;
   flex: 0 0 auto;
   height: var(--icon-size);
   margin-left: .5rem;
   margin-right: -.125rem;
-  mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0ibTEyIDEwLjYgNC45NS00Ljk2IDEuNCAxLjRMMTMuNDIgMTJsNC45NiA0Ljk1LTEuNCAxLjRMMTIgMTMuNDJsLTQuOTUgNC45Ni0xLjQtMS40TDEwLjU4IDEyIDUuNjMgNy4wNWwxLjQtMS40eiIvPjwvc3ZnPg==);
+  mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0ibTEyIDEwLjYgNC45NS00Ljk2IDEuNCAxLjRMMTMuNDIgMTJsNC45NiA0Ljk1LTEuNCAxLjRMMTIgMTMuNDJsLTQuOTUgNC45Ni0xLjQtMS40TDEwLjU4IDEyIDUuNjMgNy4wNWwxLjQtMS40eiIvPjwvc3ZnPg==");
   mask-size: 100% 100%;
-  vertical-align: calc((.75em - var(--icon-size))*.5);
+  vertical-align: calc((.75em - var(--icon-size)) * .5);
   width: var(--icon-size);
 }
 
diff --git a/css/theme/vertical-tabs.css b/css/theme/vertical-tabs.css
index 120a651..bf97ad7 100644
--- a/css/theme/vertical-tabs.css
+++ b/css/theme/vertical-tabs.css
@@ -15,6 +15,13 @@
   outline: none;
 }
 
+.vertical-tabs__menu-item.is-selected a {
+  background-color: var(--background-default-grey);
+  background-image: linear-gradient(0deg,var(--border-active-blue-france),var(--border-active-blue-france)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey));
+  background-size: 100% 2px,1px calc(100% - 1px),1px calc(100% - 1px),0 1px;
+  color: var(--text-active-blue-france);
+}
+
 .vertical-tabs__menu-item:not(.is-selected) a {
   background-color: var(--background-action-low-blue-france);
 }
@@ -23,13 +30,6 @@
   background-color: var(--background-action-low-blue-france-hover);
 }
 
-.vertical-tabs__menu-item.is-selected a {
-  background-color: var(--background-default-grey);
-  background-image: linear-gradient(0deg,var(--border-active-blue-france),var(--border-active-blue-france)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey));
-  background-size: 100% 2px,1px calc(100% - 1px),1px calc(100% - 1px),0 1px;
-  color: var(--text-active-blue-france);
-}
-
 .vertical-tabs__menu-item.is-selected .vertical-tabs__menu-item-title {
   color: inherit;
 }
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index 590ba7a..3d5621e 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -601,6 +601,9 @@ drupal.tabledrag:
       css/component/tabledrag.css: {}
   js:
     js/tabledrag.js: {}
+  dependencies:
+    - core/drupal
+    - core/jquery
 
 drupal.vertical-tabs:
   css:
diff --git a/js/horizontal-tabs.js b/js/horizontal-tabs.js
index a58d335..030a42d 100644
--- a/js/horizontal-tabs.js
+++ b/js/horizontal-tabs.js
@@ -17,19 +17,14 @@
       // Search all horizontal tabs in current context.
       once("dsfr-horizontal-tabs", ".horizontal-tabs", context).forEach((element) => {
         const wrapper = element.querySelector(`.${CLASS_TABS}`);
-        if (!wrapper) {
-          return;
-        }
-
         const list = element.querySelector(`.horizontal-tabs-list.${CLASS_LIST}`);
-        if (!list) {
+        if (!wrapper || !list) {
           return;
         }
 
         wrapper.append(list);
 
         list.querySelectorAll("li").forEach((item) => {
-
           const link = item.querySelector("a");
           link.classList.add(CLASS_TAB);
 
diff --git a/js/paragraphs.widget.js b/js/paragraphs.widget.js
index 786664b..d9bb779 100644
--- a/js/paragraphs.widget.js
+++ b/js/paragraphs.widget.js
@@ -54,7 +54,6 @@
           })
         });
       });
-
     },
   };
 
diff --git a/js/tabledrag.js b/js/tabledrag.js
index 1c3db8c..5b6ecea 100644
--- a/js/tabledrag.js
+++ b/js/tabledrag.js
@@ -9,18 +9,9 @@
  * @event columnschange
  */
 
-(function ($, Drupal, drupalSettings) {
+(function ($, Drupal) {
   "use strict";
 
-  /**
-   * Store the state of weight columns display for all tables.
-   *
-   * Default value is to hide weight columns.
-   */
-  let showWeight = JSON.parse(
-    localStorage.getItem("Drupal.tableDrag.showWeight"),
-  );
-
   const initColumnsOriginal = Drupal.tableDrag.prototype.initColumns;
   const addChangedWarningOriginal = Drupal.tableDrag.prototype.row.prototype.addChangedWarning;
 
@@ -31,13 +22,13 @@
     const $table = $(this.table.parentNode);
 
     // Do not add the changed warning if one is already present.
-    if (!$table.find('.tabledrag-changed-warning').length) {
+    if (!$table.find(".tabledrag-changed-warning").length) {
       addChangedWarningOriginal.call(this);
 
       const $tableWrapper = $table.parents(".fr-table");
 
       // Move changed warning message before DSFR table wrapper.
-      $tableWrapper.parent().prepend($table.find('.tabledrag-changed-warning'));
+      $tableWrapper.parent().prepend($table.find(".tabledrag-changed-warning"));
     }
   };
 
@@ -69,20 +60,21 @@
        */
       toggleButtonContent: (show) => {
         const classes = [
-          'tabledrag-toggle-weight',
-          'fr-icon--sm',
+          "tabledrag-toggle-weight",
+          "fr-icon--sm",
         ];
-        let text = '';
+        let text = "";
         if (show) {
-          classes.push('fr-icon-eye-off-line');
-          text = Drupal.t('Hide row weights');
-        } else {
-          classes.push('fr-icon-eye-line');
-          text = Drupal.t('Show row weights');
+          classes.push("fr-icon-eye-off-line");
+          text = Drupal.t("Hide row weights");
+        }
+ else {
+          classes.push("fr-icon-eye-line");
+          text = Drupal.t("Show row weights");
         }
-        return `<span class="${classes.join(' ')}" aria-hidden="true"></span>&nbsp;${text}`;
+        return `<span class="${classes.join(" ")}" aria-hidden="true"></span>&nbsp;${text}`;
       },
     },
   );
 
-})(jQuery, Drupal, drupalSettings);
+})(jQuery, Drupal);
-- 
GitLab


From 47e559fba2a70b803ce4d7321b8515898fdd18c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Tue, 18 Mar 2025 15:31:03 +0100
Subject: [PATCH 32/45] Fix double styling in views exposed form.

---
 composer.lock                           |  18 +
 css/component/form.css                  |   8 -
 css/component/views-exposed-form.css    |   2 +-
 vendor/autoload.php                     |  25 +
 vendor/composer/ClassLoader.php         | 579 ++++++++++++++++++++++++
 vendor/composer/InstalledVersions.php   | 359 +++++++++++++++
 vendor/composer/LICENSE                 |  21 +
 vendor/composer/autoload_classmap.php   |  10 +
 vendor/composer/autoload_namespaces.php |   9 +
 vendor/composer/autoload_psr4.php       |   9 +
 vendor/composer/autoload_real.php       |  36 ++
 vendor/composer/autoload_static.php     |  20 +
 vendor/composer/installed.json          |   5 +
 vendor/composer/installed.php           |  23 +
 14 files changed, 1115 insertions(+), 9 deletions(-)
 create mode 100644 composer.lock
 create mode 100644 vendor/autoload.php
 create mode 100644 vendor/composer/ClassLoader.php
 create mode 100644 vendor/composer/InstalledVersions.php
 create mode 100644 vendor/composer/LICENSE
 create mode 100644 vendor/composer/autoload_classmap.php
 create mode 100644 vendor/composer/autoload_namespaces.php
 create mode 100644 vendor/composer/autoload_psr4.php
 create mode 100644 vendor/composer/autoload_real.php
 create mode 100644 vendor/composer/autoload_static.php
 create mode 100644 vendor/composer/installed.json
 create mode 100644 vendor/composer/installed.php

diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..c35d1e9
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,18 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "e2b57849f956d4c67658848cdb027ed8",
+    "packages": [],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": [],
+    "plugin-api-version": "2.6.0"
+}
diff --git a/css/component/form.css b/css/component/form.css
index 4dc6fb9..7ccda7c 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -60,14 +60,6 @@ main[role="main"] > .fr-container--fluid > form {
 }
 /* Disable table scrolling into form - END */
 
-.views-exposed-form {
-  display: flex;
-  flex-wrap: wrap;
-  margin-block: var(--form-spacing);
-  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing);
-  border: 1px solid var(--border-default-grey);
-}
-
 .fr-label > .field-edit-link {
   font-size: .75em;
 }
diff --git a/css/component/views-exposed-form.css b/css/component/views-exposed-form.css
index d465cc8..3ebbefc 100644
--- a/css/component/views-exposed-form.css
+++ b/css/component/views-exposed-form.css
@@ -10,8 +10,8 @@
   display: flex;
   flex-wrap: wrap;
   margin-block: var(--form-spacing-s);
-  margin-inline: var(--form-spacing-s);
   padding: var(--form-spacing-xs) var(--form-spacing-s) var(--form-spacing-s);
+  border: 1px solid var(--border-default-grey);
 }
 
 .views-exposed-form--preview.views-exposed-form--preview {
diff --git a/vendor/autoload.php b/vendor/autoload.php
new file mode 100644
index 0000000..8d66326
--- /dev/null
+++ b/vendor/autoload.php
@@ -0,0 +1,25 @@
+<?php
+
+// autoload.php @generated by Composer
+
+if (PHP_VERSION_ID < 50600) {
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    trigger_error(
+        $err,
+        E_USER_ERROR
+    );
+}
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInite2b57849f956d4c67658848cdb027ed8::getLoader();
diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php
new file mode 100644
index 0000000..7824d8f
--- /dev/null
+++ b/vendor/composer/ClassLoader.php
@@ -0,0 +1,579 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ *     $loader = new \Composer\Autoload\ClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->add('Symfony\Component', __DIR__.'/component');
+ *     $loader->add('Symfony',           __DIR__.'/framework');
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see    https://www.php-fig.org/psr/psr-0/
+ * @see    https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+    /** @var \Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
+    private $vendorDir;
+
+    // PSR-4
+    /**
+     * @var array<string, array<string, int>>
+     */
+    private $prefixLengthsPsr4 = array();
+    /**
+     * @var array<string, list<string>>
+     */
+    private $prefixDirsPsr4 = array();
+    /**
+     * @var list<string>
+     */
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    /**
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
+     */
+    private $prefixesPsr0 = array();
+    /**
+     * @var list<string>
+     */
+    private $fallbackDirsPsr0 = array();
+
+    /** @var bool */
+    private $useIncludePath = false;
+
+    /**
+     * @var array<string, string>
+     */
+    private $classMap = array();
+
+    /** @var bool */
+    private $classMapAuthoritative = false;
+
+    /**
+     * @var array<string, bool>
+     */
+    private $missingClasses = array();
+
+    /** @var string|null */
+    private $apcuPrefix;
+
+    /**
+     * @var array<string, self>
+     */
+    private static $registeredLoaders = array();
+
+    /**
+     * @param string|null $vendorDir
+     */
+    public function __construct($vendorDir = null)
+    {
+        $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
+    }
+
+    /**
+     * @return array<string, list<string>>
+     */
+    public function getPrefixes()
+    {
+        if (!empty($this->prefixesPsr0)) {
+            return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+        }
+
+        return array();
+    }
+
+    /**
+     * @return array<string, list<string>>
+     */
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
+    }
+
+    /**
+     * @return list<string>
+     */
+    public function getFallbackDirs()
+    {
+        return $this->fallbackDirsPsr0;
+    }
+
+    /**
+     * @return list<string>
+     */
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
+    }
+
+    /**
+     * @return array<string, string> Array of classname => path
+     */
+    public function getClassMap()
+    {
+        return $this->classMap;
+    }
+
+    /**
+     * @param array<string, string> $classMap Class to filename map
+     *
+     * @return void
+     */
+    public function addClassMap(array $classMap)
+    {
+        if ($this->classMap) {
+            $this->classMap = array_merge($this->classMap, $classMap);
+        } else {
+            $this->classMap = $classMap;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
+     *
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
+     *
+     * @return void
+     */
+    public function add($prefix, $paths, $prepend = false)
+    {
+        $paths = (array) $paths;
+        if (!$prefix) {
+            if ($prepend) {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $paths,
+                    $this->fallbackDirsPsr0
+                );
+            } else {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
+                    $paths
+                );
+            }
+
+            return;
+        }
+
+        $first = $prefix[0];
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = $paths;
+
+            return;
+        }
+        if ($prepend) {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $paths,
+                $this->prefixesPsr0[$first][$prefix]
+            );
+        } else {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
+                $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
+     *
+     * @param string              $prefix  The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
+     *
+     * @throws \InvalidArgumentException
+     *
+     * @return void
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        $paths = (array) $paths;
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
+     *
+     * @return void
+     */
+    public function set($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string              $prefix The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths  The PSR-4 base directories
+     *
+     * @throws \InvalidArgumentException
+     *
+     * @return void
+     */
+    public function setPsr4($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Turns on searching the include path for class files.
+     *
+     * @param bool $useIncludePath
+     *
+     * @return void
+     */
+    public function setUseIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return bool
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Turns off searching the prefix and fallback directories for classes
+     * that have not been registered with the class map.
+     *
+     * @param bool $classMapAuthoritative
+     *
+     * @return void
+     */
+    public function setClassMapAuthoritative($classMapAuthoritative)
+    {
+        $this->classMapAuthoritative = $classMapAuthoritative;
+    }
+
+    /**
+     * Should class lookup fail if not found in the current class map?
+     *
+     * @return bool
+     */
+    public function isClassMapAuthoritative()
+    {
+        return $this->classMapAuthoritative;
+    }
+
+    /**
+     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+     *
+     * @param string|null $apcuPrefix
+     *
+     * @return void
+     */
+    public function setApcuPrefix($apcuPrefix)
+    {
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+    }
+
+    /**
+     * The APCu prefix in use, or null if APCu caching is not enabled.
+     *
+     * @return string|null
+     */
+    public function getApcuPrefix()
+    {
+        return $this->apcuPrefix;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not
+     *
+     * @return void
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+        if (null === $this->vendorDir) {
+            return;
+        }
+
+        if ($prepend) {
+            self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+        } else {
+            unset(self::$registeredLoaders[$this->vendorDir]);
+            self::$registeredLoaders[$this->vendorDir] = $this;
+        }
+    }
+
+    /**
+     * Unregisters this instance as an autoloader.
+     *
+     * @return void
+     */
+    public function unregister()
+    {
+        spl_autoload_unregister(array($this, 'loadClass'));
+
+        if (null !== $this->vendorDir) {
+            unset(self::$registeredLoaders[$this->vendorDir]);
+        }
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param  string    $class The name of the class
+     * @return true|null True if loaded, null otherwise
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            $includeFile = self::$includeFile;
+            $includeFile($file);
+
+            return true;
+        }
+
+        return null;
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|false The path if found, false otherwise
+     */
+    public function findFile($class)
+    {
+        // class map lookup
+        if (isset($this->classMap[$class])) {
+            return $this->classMap[$class];
+        }
+        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+            return false;
+        }
+        if (null !== $this->apcuPrefix) {
+            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+            if ($hit) {
+                return $file;
+            }
+        }
+
+        $file = $this->findFileWithExtension($class, '.php');
+
+        // Search for Hack files if we are running on HHVM
+        if (false === $file && defined('HHVM_VERSION')) {
+            $file = $this->findFileWithExtension($class, '.hh');
+        }
+
+        if (null !== $this->apcuPrefix) {
+            apcu_add($this->apcuPrefix.$class, $file);
+        }
+
+        if (false === $file) {
+            // Remember that this class does not exist.
+            $this->missingClasses[$class] = true;
+        }
+
+        return $file;
+    }
+
+    /**
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
+     *
+     * @return array<string, self>
+     */
+    public static function getRegisteredLoaders()
+    {
+        return self::$registeredLoaders;
+    }
+
+    /**
+     * @param  string       $class
+     * @param  string       $ext
+     * @return string|false
+     */
+    private function findFileWithExtension($class, $ext)
+    {
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            $subPath = $class;
+            while (false !== $lastPos = strrpos($subPath, '\\')) {
+                $subPath = substr($subPath, 0, $lastPos);
+                $search = $subPath . '\\';
+                if (isset($this->prefixDirsPsr4[$search])) {
+                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
+                        if (file_exists($file = $dir . $pathEnd)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+        }
+
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+            return $file;
+        }
+
+        return false;
+    }
+
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = \Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
+}
diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php
new file mode 100644
index 0000000..51e734a
--- /dev/null
+++ b/vendor/composer/InstalledVersions.php
@@ -0,0 +1,359 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
+ */
+class InstalledVersions
+{
+    /**
+     * @var mixed[]|null
+     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
+     */
+    private static $installed;
+
+    /**
+     * @var bool|null
+     */
+    private static $canGetVendors;
+
+    /**
+     * @var array[]
+     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    private static $installedByVendor = array();
+
+    /**
+     * Returns a list of all package names which are present, either by being installed, replaced or provided
+     *
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackages()
+    {
+        $packages = array();
+        foreach (self::getInstalled() as $installed) {
+            $packages[] = array_keys($installed['versions']);
+        }
+
+        if (1 === \count($packages)) {
+            return $packages[0];
+        }
+
+        return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
+    }
+
+    /**
+     * Returns a list of all package names with a specific type e.g. 'library'
+     *
+     * @param  string   $type
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackagesByType($type)
+    {
+        $packagesByType = array();
+
+        foreach (self::getInstalled() as $installed) {
+            foreach ($installed['versions'] as $name => $package) {
+                if (isset($package['type']) && $package['type'] === $type) {
+                    $packagesByType[] = $name;
+                }
+            }
+        }
+
+        return $packagesByType;
+    }
+
+    /**
+     * Checks whether the given package is installed
+     *
+     * This also returns true if the package name is provided or replaced by another package
+     *
+     * @param  string $packageName
+     * @param  bool   $includeDevRequirements
+     * @return bool
+     */
+    public static function isInstalled($packageName, $includeDevRequirements = true)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (isset($installed['versions'][$packageName])) {
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks whether the given package satisfies a version constraint
+     *
+     * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+     *
+     *   Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+     *
+     * @param  VersionParser $parser      Install composer/semver to have access to this class and functionality
+     * @param  string        $packageName
+     * @param  string|null   $constraint  A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+     * @return bool
+     */
+    public static function satisfies(VersionParser $parser, $packageName, $constraint)
+    {
+        $constraint = $parser->parseConstraints((string) $constraint);
+        $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+        return $provided->matches($constraint);
+    }
+
+    /**
+     * Returns a version constraint representing all the range(s) which are installed for a given package
+     *
+     * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+     * whether a given version of a package is installed, and not just whether it exists
+     *
+     * @param  string $packageName
+     * @return string Version constraint usable with composer/semver
+     */
+    public static function getVersionRanges($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            $ranges = array();
+            if (isset($installed['versions'][$packageName]['pretty_version'])) {
+                $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+            }
+            if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+            }
+            if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+            }
+            if (array_key_exists('provided', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+            }
+
+            return implode(' || ', $ranges);
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getVersion($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['version'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['version'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getPrettyVersion($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['pretty_version'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+     */
+    public static function getReference($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['reference'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['reference'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+     */
+    public static function getInstallPath($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @return array
+     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+     */
+    public static function getRootPackage()
+    {
+        $installed = self::getInstalled();
+
+        return $installed[0]['root'];
+    }
+
+    /**
+     * Returns the raw installed.php data for custom implementations
+     *
+     * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+     * @return array[]
+     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
+     */
+    public static function getRawData()
+    {
+        @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+        if (null === self::$installed) {
+            // only require the installed.php file if this file is loaded from its dumped location,
+            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+            if (substr(__DIR__, -8, 1) !== 'C') {
+                self::$installed = include __DIR__ . '/installed.php';
+            } else {
+                self::$installed = array();
+            }
+        }
+
+        return self::$installed;
+    }
+
+    /**
+     * Returns the raw data of all installed.php which are currently loaded for custom implementations
+     *
+     * @return array[]
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    public static function getAllRawData()
+    {
+        return self::getInstalled();
+    }
+
+    /**
+     * Lets you reload the static array from another file
+     *
+     * This is only useful for complex integrations in which a project needs to use
+     * this class but then also needs to execute another project's autoloader in process,
+     * and wants to ensure both projects have access to their version of installed.php.
+     *
+     * A typical case would be PHPUnit, where it would need to make sure it reads all
+     * the data it needs from this class, then call reload() with
+     * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+     * the project in which it runs can then also use this class safely, without
+     * interference between PHPUnit's dependencies and the project's dependencies.
+     *
+     * @param  array[] $data A vendor/composer/installed.php data set
+     * @return void
+     *
+     * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
+     */
+    public static function reload($data)
+    {
+        self::$installed = $data;
+        self::$installedByVendor = array();
+    }
+
+    /**
+     * @return array[]
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    private static function getInstalled()
+    {
+        if (null === self::$canGetVendors) {
+            self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+        }
+
+        $installed = array();
+
+        if (self::$canGetVendors) {
+            foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+                if (isset(self::$installedByVendor[$vendorDir])) {
+                    $installed[] = self::$installedByVendor[$vendorDir];
+                } elseif (is_file($vendorDir.'/composer/installed.php')) {
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    $installed[] = self::$installedByVendor[$vendorDir] = $required;
+                    if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
+                        self::$installed = $installed[count($installed) - 1];
+                    }
+                }
+            }
+        }
+
+        if (null === self::$installed) {
+            // only require the installed.php file if this file is loaded from its dumped location,
+            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+            if (substr(__DIR__, -8, 1) !== 'C') {
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
+            } else {
+                self::$installed = array();
+            }
+        }
+
+        if (self::$installed !== array()) {
+            $installed[] = self::$installed;
+        }
+
+        return $installed;
+    }
+}
diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE
new file mode 100644
index 0000000..f27399a
--- /dev/null
+++ b/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php
new file mode 100644
index 0000000..0fb0a2c
--- /dev/null
+++ b/vendor/composer/autoload_classmap.php
@@ -0,0 +1,10 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
+);
diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php
new file mode 100644
index 0000000..15a2ff3
--- /dev/null
+++ b/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+);
diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php
new file mode 100644
index 0000000..3890ddc
--- /dev/null
+++ b/vendor/composer/autoload_psr4.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+);
diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php
new file mode 100644
index 0000000..5c6bdc2
--- /dev/null
+++ b/vendor/composer/autoload_real.php
@@ -0,0 +1,36 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInite2b57849f956d4c67658848cdb027ed8
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    /**
+     * @return \Composer\Autoload\ClassLoader
+     */
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        spl_autoload_register(array('ComposerAutoloaderInite2b57849f956d4c67658848cdb027ed8', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
+        spl_autoload_unregister(array('ComposerAutoloaderInite2b57849f956d4c67658848cdb027ed8', 'loadClassLoader'));
+
+        require __DIR__ . '/autoload_static.php';
+        call_user_func(\Composer\Autoload\ComposerStaticInite2b57849f956d4c67658848cdb027ed8::getInitializer($loader));
+
+        $loader->register(true);
+
+        return $loader;
+    }
+}
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
new file mode 100644
index 0000000..3e4da4b
--- /dev/null
+++ b/vendor/composer/autoload_static.php
@@ -0,0 +1,20 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInite2b57849f956d4c67658848cdb027ed8
+{
+    public static $classMap = array (
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+    );
+
+    public static function getInitializer(ClassLoader $loader)
+    {
+        return \Closure::bind(function () use ($loader) {
+            $loader->classMap = ComposerStaticInite2b57849f956d4c67658848cdb027ed8::$classMap;
+
+        }, null, ClassLoader::class);
+    }
+}
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
new file mode 100644
index 0000000..87fda74
--- /dev/null
+++ b/vendor/composer/installed.json
@@ -0,0 +1,5 @@
+{
+    "packages": [],
+    "dev": true,
+    "dev-package-names": []
+}
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
new file mode 100644
index 0000000..61d003e
--- /dev/null
+++ b/vendor/composer/installed.php
@@ -0,0 +1,23 @@
+<?php return array(
+    'root' => array(
+        'name' => 'drupal/dsfr4drupal',
+        'pretty_version' => '1.x-dev',
+        'version' => '1.9999999.9999999.9999999-dev',
+        'reference' => 'c48cfd677155caefea8b2a0c84ffe527588e0484',
+        'type' => 'drupal-theme',
+        'install_path' => __DIR__ . '/../../',
+        'aliases' => array(),
+        'dev' => true,
+    ),
+    'versions' => array(
+        'drupal/dsfr4drupal' => array(
+            'pretty_version' => '1.x-dev',
+            'version' => '1.9999999.9999999.9999999-dev',
+            'reference' => 'c48cfd677155caefea8b2a0c84ffe527588e0484',
+            'type' => 'drupal-theme',
+            'install_path' => __DIR__ . '/../../',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+    ),
+);
-- 
GitLab


From 0eccc9430bd0c425ea7454e85d5ce7a1b3549421 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Tue, 18 Mar 2025 16:17:24 +0100
Subject: [PATCH 33/45] Manage base page error 500.

---
 templates/base/base-page-404.html.twig |  2 +-
 templates/base/base-page-500.html.twig | 38 ++++++++++++++++++++++++++
 translations/fr.po                     | 15 ++++++++++
 3 files changed, 54 insertions(+), 1 deletion(-)
 create mode 100644 templates/base/base-page-500.html.twig

diff --git a/templates/base/base-page-404.html.twig b/templates/base/base-page-404.html.twig
index a0ab027..17535f6 100644
--- a/templates/base/base-page-404.html.twig
+++ b/templates/base/base-page-404.html.twig
@@ -1,6 +1,6 @@
 {% set links = links|default([
   {
-    'url': url('<front>')|render|render,
+    'url': 'internal:/<front>',
     'title': 'Homepage'|t,
   },
 ]) %}
diff --git a/templates/base/base-page-500.html.twig b/templates/base/base-page-500.html.twig
new file mode 100644
index 0000000..4180e5b
--- /dev/null
+++ b/templates/base/base-page-500.html.twig
@@ -0,0 +1,38 @@
+{% set links = links|default([
+  {
+    'url': 'internal:/contact',
+    'title': 'Contact us'|t,
+  },
+]) %}
+{% set subtitle = subtitle|default('Error 500'|t) %}
+{% set title = title|default('Unexpected error'|t) %}
+{% set text_lead = text_lead|default('Sorry, there is an issue with the service, we are working to resolve it as quickly as possible.'|t) %}
+{% set text = text|default('Please try refreshing the page or try again later.'|t) %}
+
+<div class="fr-py-0 fr-col-12 fr-col-md-6">
+  <h1>{{ title }}</h1>
+  <p class="fr-text--sm fr-mb-3w">{{ subtitle }}</p>
+  <p class="fr-text--lead fr-mb-3w">{{ text_lead }}</p>
+  <p class="fr-text--sm fr-mb-5w">{{ text }}</p>
+  {% if links %}
+    <ul class="fr-btns-group fr-btns-group--inline-md">
+      {% for link in links %}
+        <li>
+          {{ link(link.title, link.url, {'class': 'fr-btn'}) }}
+        </li>
+      {% endfor %}
+    </ul>
+    {{ attach_library('dsfr4drupal/component.button') }}
+  {% endif %}
+</div>
+<div class="fr-col-12 fr-col-md-3 fr-col-offset-md-1 fr-px-6w fr-px-md-0 fr-py-0">
+  <svg xmlns="http://www.w3.org/2000/svg" class="fr-responsive-img fr-artwork" aria-hidden="true" width="160" height="200" viewBox="0 0 160 200">
+    <use class="fr-artwork-motif" href="{{ dsfr_path }}artwork/background/ovoid.svg#artwork-motif"></use>
+    <use class="fr-artwork-background" href="{{ dsfr_path }}artwork/background/ovoid.svg#artwork-background"></use>
+    <g transform="translate(40, 60)">
+      <use class="fr-artwork-decorative" href="{{ dsfr_path }}artwork/pictograms/system/technical-error.svg#artwork-decorative"></use>
+      <use class="fr-artwork-minor" href="{{ dsfr_path }}artwork/pictograms/system/technical-error.svg#artwork-minor"></use>
+      <use class="fr-artwork-major" href="{{ dsfr_path }}artwork/pictograms/system/technical-error.svg#artwork-major"></use>
+    </g>
+  </svg>
+</div>
diff --git a/translations/fr.po b/translations/fr.po
index e3ef920..792f25d 100644
--- a/translations/fr.po
+++ b/translations/fr.po
@@ -268,6 +268,9 @@ msgstr "@code - @label"
 msgid "Page not found"
 msgstr "Page non trouvée"
 
+msgid "Unexpected error"
+msgstr "Erreur inattendue"
+
 msgid "Show"
 msgstr "Afficher"
 
@@ -277,9 +280,15 @@ msgstr "Afficher le mot de passe"
 msgid "Error 404"
 msgstr "Erreur 404"
 
+msgid "Error 500"
+msgstr "Erreur 500"
+
 msgid "The page you are looking for cannot be found. We apologize for the inconvenience caused."
 msgstr "La page que vous cherchez est introuvable. Excusez-nous pour la gène occasionnée."
 
+msgid "Sorry, there is an issue with the service, we are working to resolve it as quickly as possible."
+msgstr "Désolé, le service rencontre un problème, nous travaillons pour le résoudre le plus rapidement possible."
+
 msgid ""
 "If you typed the web address into the browser, verify that it is correct. "
 "The page may no longer be available.<br />In this case, to continue your visit you can consult our home page, "
@@ -289,6 +298,12 @@ msgstr ""
 "La page n’est peut-être plus disponible.<br />Dans ce cas, pour continuer votre visite vous pouvez consulter notre page d’accueil, "
 "ou effectuer une recherche avec notre moteur de recherche en haut de page.<br />Sinon contactez-nous pour que l’on puisse vous rediriger vers la bonne information."
 
+msgid "Please try refreshing the page or try again later."
+msgstr "Essayez de rafraîchir la page ou bien ressayez plus tard."
+
+msgid "Contact us"
+msgstr "Contactez-nous"
+
 msgid "Copy to clipboard"
 msgstr "Copier dans le presse papier"
 
-- 
GitLab


From b7bfb94b2483b1437a0393e555bd782ea67313b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Tue, 18 Mar 2025 16:44:34 +0100
Subject: [PATCH 34/45] Manage local action styles.

---
 css/component/action-links.css                  | 17 +++++++++++++++++
 dsfr4drupal.libraries.yml                       |  5 +++++
 includes/menu.theme                             | 10 ++++++++++
 .../block/block--local-actions-block.html.twig  | 10 ++++++++++
 templates/menu/menu-local-tasks.html.twig       |  2 ++
 5 files changed, 44 insertions(+)
 create mode 100644 css/component/action-links.css
 create mode 100644 templates/block/block--local-actions-block.html.twig

diff --git a/css/component/action-links.css b/css/component/action-links.css
new file mode 100644
index 0000000..3cb17c6
--- /dev/null
+++ b/css/component/action-links.css
@@ -0,0 +1,17 @@
+/**
+ * @file
+ * Styles for local action links.
+*/
+
+.local-actions {
+  --local-action-margin: 1rem;
+
+  list-style: none;
+  margin: 0 calc(var(--local-action-margin) * -1);
+  padding: 0;
+}
+
+.local-actions li {
+  display: inline-block;
+  margin: 0 var(--local-action-margin);
+}
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index 3d5621e..9d55cd0 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -620,6 +620,11 @@ element.horizontal_tabs:
     - core/once
     - dsfr4drupal/component.tab
 
+local-actions:
+  css:
+    component:
+      css/component/action-links.css: {}
+
 #legacy:
 #  js:
 #    /libraries/dsfr/dist/legacy/legacy.nomodule.min.js:
diff --git a/includes/menu.theme b/includes/menu.theme
index 36748b1..08f0483 100644
--- a/includes/menu.theme
+++ b/includes/menu.theme
@@ -26,3 +26,13 @@ function dsfr4drupal_preprocess_menu(array &$variables): void {
   // Define menu "aria-label".
   $variables['aria_label'] = $menu ? $menu->label() : '';
 }
+
+/**
+ * Implements hook_preprocess_HOOK() for "menu".
+ */
+function dsfr4drupal_preprocess_menu_local_action(array &$variables): void {
+  $link = &$variables['link'];
+
+  $link['#options']['attributes']['class'][] = 'fr-btn';
+  $link['#attached']['library'][] = 'dsfr4drupal/component.button';
+}
diff --git a/templates/block/block--local-actions-block.html.twig b/templates/block/block--local-actions-block.html.twig
new file mode 100644
index 0000000..c117571
--- /dev/null
+++ b/templates/block/block--local-actions-block.html.twig
@@ -0,0 +1,10 @@
+{% extends 'block.html.twig' %}
+
+{% block content %}
+  {% if content %}
+    <ul class="local-actions">
+      {{ content }}
+    </ul>
+    {{ attach_library('dsfr4drupal/local-actions') }}
+  {% endif %}
+{% endblock %}
diff --git a/templates/menu/menu-local-tasks.html.twig b/templates/menu/menu-local-tasks.html.twig
index 90c4ed7..22097ad 100644
--- a/templates/menu/menu-local-tasks.html.twig
+++ b/templates/menu/menu-local-tasks.html.twig
@@ -1,3 +1,5 @@
+{% set attributes = attributes.addClass('fr-mb-4v') %}
+
 {% if primary %}
   <h2 class="visually-hidden">{{ 'Primary tabs'|t }}</h2>
   {{ include('dsfr4drupal:tabs', {'tabs': primary}) }}
-- 
GitLab


From 4cc13d2fbca7703f606a231329e83258de6da9b3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 19 Mar 2025 14:17:02 +0100
Subject: [PATCH 35/45] Add missing expanded default value.

---
 templates/system/details.html.twig | 1 +
 1 file changed, 1 insertion(+)

diff --git a/templates/system/details.html.twig b/templates/system/details.html.twig
index 991c919..f9dce31 100644
--- a/templates/system/details.html.twig
+++ b/templates/system/details.html.twig
@@ -12,5 +12,6 @@
 
 {{ include('dsfr4drupal:accordion', {
   'button_attributes': create_attribute({'class': required ? ['js-form-required', 'form-required'] : ''}),
+  'expanded': not element['#attributes']['open'] is empty,
   'title_tag': 'div',
 }) }}
-- 
GitLab


From 0a3d88e2041e2c744ae4926c642bb5eb52069fb6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 19 Mar 2025 14:26:59 +0100
Subject: [PATCH 36/45] Manage dropbutton.

---
 css/component/checkbox.css                 |  4 +-
 css/component/dropbutton.css               | 34 ++++++++++
 css/component/table.css                    | 14 ++++
 css/theme/media-library.css                |  5 ++
 dsfr4drupal.info.yml                       |  2 +
 dsfr4drupal.libraries.yml                  | 11 ++++
 js/dropbutton.js                           | 76 ++++++++++++++++++++++
 templates/form/links--dropbutton.html.twig | 63 ++++++++++++++++++
 8 files changed, 207 insertions(+), 2 deletions(-)
 create mode 100644 css/component/dropbutton.css
 create mode 100644 css/component/table.css
 create mode 100644 js/dropbutton.js
 create mode 100644 templates/form/links--dropbutton.html.twig

diff --git a/css/component/checkbox.css b/css/component/checkbox.css
index 412003e..462f787 100644
--- a/css/component/checkbox.css
+++ b/css/component/checkbox.css
@@ -7,8 +7,8 @@
 .fr-checkbox-group input[type="checkbox"] + label.visually-hidden {
   width: 1.5rem;
   height: 1.5rem;
-  margin-left: .5rem;
-  margin-top: .5rem;
+  margin-left: 0;
+  margin-top: 0;
   padding-left: 1.5rem;
   clip: auto;
   z-index: 1;
diff --git a/css/component/dropbutton.css b/css/component/dropbutton.css
new file mode 100644
index 0000000..1ac8298
--- /dev/null
+++ b/css/component/dropbutton.css
@@ -0,0 +1,34 @@
+/**
+ * @file
+ * Manage styles for dropbutton.
+ */
+
+html.js .dropbutton-wrapper .dropbutton .secondary-action {
+  display: block;
+}
+
+.dropbutton-wrapper:not(.open) .dropbutton__item:first-of-type ~ .dropbutton__item {
+  visibility: hidden;
+  /**
+  * By setting a height of 1px, the dropbutton items are hidden
+  * from view while still occupying minimal space, ensuring the layout remains intact.
+  */
+  height: 1px;
+}
+
+.dropbutton__items {
+  position: fixed;
+  padding: 0;
+  background-color: var(--background-default-grey);
+}
+.dropbutton__items a {
+  margin-right: 2em;
+}
+
+.dropbutton li {
+  padding-bottom: 0;
+}
+
+.js td .dropbutton-multiple {
+  padding-right: 0;
+}
diff --git a/css/component/table.css b/css/component/table.css
new file mode 100644
index 0000000..962213b
--- /dev/null
+++ b/css/component/table.css
@@ -0,0 +1,14 @@
+/**
+ * @file
+ * Manage styles for table.
+ */
+
+.fr-table__content td .fr-checkbox-group,
+.fr-table__content th .fr-checkbox-group {
+  vertical-align: top;
+}
+
+.fr-table__content th.select-all input[type="checkbox"] {
+  width: 1.5rem;
+  height: 1.5rem;
+}
diff --git a/css/theme/media-library.css b/css/theme/media-library.css
index e5d4c7b..772f706 100644
--- a/css/theme/media-library.css
+++ b/css/theme/media-library.css
@@ -36,3 +36,8 @@
   display: inline-block;
   font-weight: 700;
 }
+
+.views-field-media-library-select-form .fr-checkbox-group input[type="checkbox"] + label.visually-hidden {
+  margin-left: .5rem;
+  margin-top: .5rem;
+}
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index a563b75..bce1acd 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -38,6 +38,8 @@ libraries:
 libraries-extend:
   core/drupal.dialog:
     - dsfr4drupal/drupal.dialog
+  core/drupal.dropbutton:
+    - dsfr4drupal/drupal.dropbutton
   core/drupal.form:
     - dsfr4drupal/drupal.form
   core/drupal.message:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index 9d55cd0..dc72f62 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -401,6 +401,7 @@ component.table:
   css:
     component:
       /libraries/dsfr/dist/component/table/table.min.css: { minified: true }
+      css/component/table.css: {}
   js:
     /libraries/dsfr/dist/component/table/table.module.min.js:
       minified: true
@@ -558,6 +559,16 @@ drupal.dialog:
       # Need to fix weight to 99 to override "Gin toolbar" styles.
       css/theme/ui-dialog.css: { weight: 100 }
 
+drupal.dropbutton:
+  css:
+    component:
+      css/component/dropbutton.css: {}
+  js:
+    js/dropbutton.js: {}
+  dependencies:
+    - core/drupal
+    - core/once
+
 drupal.form:
   css:
     component:
diff --git a/js/dropbutton.js b/js/dropbutton.js
new file mode 100644
index 0000000..403cddd
--- /dev/null
+++ b/js/dropbutton.js
@@ -0,0 +1,76 @@
+/**
+ * @file
+ * Manage "dropbutton" behaviors.
+ */
+
+((Drupal, once) => {
+  Drupal.behaviors.dsfr4drupalDropbutton = {
+    attach: function (context) {
+      once("dsfr4drupal-dropbutton", ".dropbutton-multiple:has(.dropbutton--dsfr4drupal)", context).forEach(element => {
+        element.querySelector(".dropbutton-toggle").addEventListener("click", () => {
+          this.updatePosition(element);
+
+          window.addEventListener("scroll", () => Drupal.debounce(this.updatePositionIfOpen(element), 100));
+          window.addEventListener("resize", () => Drupal.debounce(this.updatePositionIfOpen(element), 100));
+        });
+      });
+    },
+
+    updatePosition: (element) => {
+      const preferredDir = document.documentElement.dir ?? "ltr";
+      const secondaryAction = element.querySelector(".secondary-action");
+      const dropMenu = secondaryAction.querySelector(".dropbutton__items");
+      const toggleHeight = element.offsetHeight;
+      const dropMenuWidth = dropMenu.offsetWidth;
+      const dropMenuHeight = dropMenu.offsetHeight;
+      const boundingRect = secondaryAction.getBoundingClientRect();
+      const spaceBelow = window.innerHeight - boundingRect.bottom;
+      const spaceLeft = boundingRect.left;
+      const spaceRight = window.innerWidth - boundingRect.right;
+
+      // Calculate the menu position based on available space and the preferred
+      // reading direction.
+      const leftAlignStyles = {
+        left: `${boundingRect.left}px`,
+        right: "auto"
+      };
+      const rightAlignStyles = {
+        left: "auto",
+        right: `${window.innerWidth - boundingRect.right}px`
+      };
+
+      if (preferredDir === "ltr") {
+        if (spaceRight >= dropMenuWidth) {
+          Object.assign(dropMenu.style, leftAlignStyles);
+        }
+        else {
+          Object.assign(dropMenu.style, rightAlignStyles);
+        }
+      }
+      else {
+        if (spaceLeft >= dropMenuWidth) {
+          Object.assign(dropMenu.style, rightAlignStyles);
+        }
+        else {
+          Object.assign(dropMenu.style, leftAlignStyles);
+        }
+      }
+
+      if (spaceBelow >= dropMenuHeight) {
+        dropMenu.style.top = `${boundingRect.bottom}px`;
+      }
+      else {
+        dropMenu.style.top = `${boundingRect.top - toggleHeight - dropMenuHeight}px`
+      }
+
+    },
+
+    updatePositionIfOpen: (element) => {
+      if (element.classList.contains("open")) {
+        this.updatePosition(element);
+      }
+    },
+
+  };
+
+})(Drupal, once);
diff --git a/templates/form/links--dropbutton.html.twig b/templates/form/links--dropbutton.html.twig
new file mode 100644
index 0000000..0b94975
--- /dev/null
+++ b/templates/form/links--dropbutton.html.twig
@@ -0,0 +1,63 @@
+{#
+/**
+ * @file
+ * Theme override for a set of links.
+ *
+ * Available variables:
+ * - attributes: Attributes for the UL containing the list of links.
+ * - links: Links to be output.
+ *   Each link will have the following elements:
+ *   - link: (optional) A render array that returns a link. See
+ *     template_preprocess_links() for details how it is generated.
+ *   - text: The link text.
+ *   - attributes: HTML attributes for the list item element.
+ *   - text_attributes: (optional) HTML attributes for the span element if no
+ *     'url' was supplied.
+ * - heading: (optional) A heading to precede the links.
+ *   - text: The heading text.
+ *   - level: The heading level (e.g. 'h2', 'h3').
+ *   - attributes: (optional) A keyed list of attributes for the heading.
+ *   If the heading is a string, it will be used as the text of the heading and
+ *   the level will default to 'h2'.
+ *
+ *   Headings should be used on navigation menus and any list of links that
+ *   consistently appears on multiple pages. To make the heading invisible use
+ *   the 'visually-hidden' CSS class. Do not use 'display:none', which
+ *   removes it from screen readers and assistive technology. Headings allow
+ *   screen reader and keyboard only users to navigate to or skip the links.
+ *   See http://juicystudio.com/article/screen-readers-display-none.php and
+ *   http://www.w3.org/TR/WCAG-TECHS/H42.html for more information.
+ *
+ * @see template_preprocess_links()
+ */
+#}
+{% if links -%}
+  {%- if heading -%}
+    {%- if heading.level -%}
+      <{{ heading.level }}{{ heading.attributes }}>{{ heading.text }}</{{ heading.level }}>
+    {%- else -%}
+      <h2{{ heading.attributes }}>{{ heading.text }}</h2>
+    {%- endif -%}
+  {%- endif -%}
+  <ul{{ attributes.addClass('dropbutton--dsfr4drupal') }}>
+    {%- for item in links -%}
+      {% if loop.index == 2 %}
+      <li class="dropbutton__item">
+        <ul class="dropbutton__items">
+      {% endif %}
+      <li{{ item.attributes.addClass('dropbutton__item') }}>
+        {%- if item.link -%}
+          {{ item.link }}
+        {%- elseif item.text_attributes -%}
+          <span{{ item.text_attributes }}>{{ item.text }}</span>
+        {%- else -%}
+          {{ item.text }}
+        {%- endif -%}
+      </li>
+      {% if loop.length > 1 and loop.last %}
+        </ul>
+      </li>
+      {% endif %}
+    {%- endfor -%}
+  </ul>
+{%- endif %}
-- 
GitLab


From 58260cf983f8d5bef77d504c13beb993dc100245 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 19 Mar 2025 16:37:41 +0100
Subject: [PATCH 37/45] Stylize system admin pages.

---
 css/theme/system.admin.css                    | 23 +++++++++++++++++++
 dsfr4drupal.info.yml                          |  2 ++
 dsfr4drupal.libraries.yml                     |  5 ++++
 .../system/admin-block-content.html.twig      | 14 +++++++++++
 templates/system/admin-page.html.twig         | 13 +++++++++++
 5 files changed, 57 insertions(+)
 create mode 100644 css/theme/system.admin.css
 create mode 100644 templates/system/admin-block-content.html.twig
 create mode 100644 templates/system/admin-page.html.twig

diff --git a/css/theme/system.admin.css b/css/theme/system.admin.css
new file mode 100644
index 0000000..510c76b
--- /dev/null
+++ b/css/theme/system.admin.css
@@ -0,0 +1,23 @@
+/**
+ * @file
+ * Manage styles for system admin.
+ */
+
+.compact-link {
+  text-align: right;
+}
+
+.panel {
+  padding: 1rem 1rem .5rem;
+  border: 1px solid var(--border-default-grey);
+}
+
+.panel + .panel {
+  margin-top: 1.5rem;
+}
+
+.list-group dd + dt {
+  margin-top: .25rem;
+  padding-top: .5rem;
+  border-top: 1px solid var(--border-default-grey);
+}
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index bce1acd..84f10ba 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -66,6 +66,8 @@ libraries-extend:
     - dsfr4drupal/drupal.paragraphs.widget
   select2/select2:
     - dsfr4drupal/select2
+  system/admin:
+    - dsfr4drupal/admin
   tarte_au_citron/tarte_au_citron_lib:
     - dsfr4drupal/tarteaucitron
   tacjs/tarteaucitron.js:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index dc72f62..7d080f5 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -2,6 +2,11 @@
 # These Javascript files are needed for old/outdated browsers.
 # @see: https://www.systeme-de-design.gouv.fr/utilisation-et-organisation/developpeurs/prise-en-main/
 
+admin:
+  css:
+    theme:
+      css/theme/system.admin.css: {}
+
 component.accordion:
   css:
     component:
diff --git a/templates/system/admin-block-content.html.twig b/templates/system/admin-block-content.html.twig
new file mode 100644
index 0000000..c3f4433
--- /dev/null
+++ b/templates/system/admin-block-content.html.twig
@@ -0,0 +1,14 @@
+{% set classes = [
+  'list-group',
+  compact ? 'compact',
+] %}
+{% if content %}
+  <dl{{ attributes.addClass(classes) }}>
+    {% for item in content %}
+      <dt class="list-group__link fr-mt-2v">{{ item.link }}</dt>
+      {% if item.description %}
+        <dd class="list-group__description">{{ item.description }}</dd>
+      {% endif %}
+    {% endfor %}
+  </dl>
+{% endif %}
diff --git a/templates/system/admin-page.html.twig b/templates/system/admin-page.html.twig
new file mode 100644
index 0000000..b82b265
--- /dev/null
+++ b/templates/system/admin-page.html.twig
@@ -0,0 +1,13 @@
+<div class="clearfix">
+  {{ system_compact_link|add_class('fr-text--sm') }}
+
+  <div class="fr-grid-row fr-grid-row--gutters">
+    {% for container in containers %}
+      <div class="fr-col-12 fr-col-md-6">
+        {% for block in container.blocks %}
+          {{ block }}
+        {% endfor %}
+      </div>
+    {% endfor %}
+  </div>
+</div>
-- 
GitLab


From aa3951df9f1c6144b71522761c8144b93ea94b34 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 19 Mar 2025 17:12:00 +0100
Subject: [PATCH 38/45] Fix checkbox group spacing.

---
 css/component/dropbutton.css                  |  6 +-
 css/component/form.css                        | 17 ++++++
 dsfr4drupal.info.yml                          |  2 -
 dsfr4drupal.libraries.yml                     |  6 +-
 .../status-report-general-info.html.twig      | 61 +++++++++++++++++++
 5 files changed, 84 insertions(+), 8 deletions(-)
 create mode 100644 templates/system/status-report-general-info.html.twig

diff --git a/css/component/dropbutton.css b/css/component/dropbutton.css
index 1ac8298..eb147fd 100644
--- a/css/component/dropbutton.css
+++ b/css/component/dropbutton.css
@@ -18,7 +18,7 @@ html.js .dropbutton-wrapper .dropbutton .secondary-action {
 
 .dropbutton__items {
   position: fixed;
-  padding: 0;
+  padding: .25rem;
   background-color: var(--background-default-grey);
 }
 .dropbutton__items a {
@@ -32,3 +32,7 @@ html.js .dropbutton-wrapper .dropbutton .secondary-action {
 .js td .dropbutton-multiple {
   padding-right: 0;
 }
+
+.js .dropbutton li, .js .dropbutton a {
+  display: inline-block;
+}
diff --git a/css/component/form.css b/css/component/form.css
index 7ccda7c..a94449c 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -26,6 +26,18 @@
   --form-spacing: 0;
 }
 
+/*Remove specific spacing into fieldset - START */
+.fr-fieldset__content .fr-checkbox-group input[type="checkbox"] + label::before,
+.fr-fieldset__content .fr-radio-group:not(.fr-radio-rich) input[type="radio"] + label::before {
+  top: auto;
+}
+
+.fr-fieldset__content .fr-checkbox-group label,
+.fr-fieldset__content .fr-radio-group label {
+  padding: 0;
+}
+/*Remove specific spacing into fieldset - END */
+
 main[role="main"] > .fr-container > form,
 main[role="main"] > .fr-container--fluid > form {
   margin-block: var(--form-spacing);
@@ -35,6 +47,11 @@ main[role="main"] > .fr-container--fluid > form {
   margin-block: var(--form-item-spacing);
 }
 
+.fr-checkbox-group + .fr-checkbox-group,
+.fr-radio-group + .fr-radio-group {
+  margin-top:  .25rem;
+}
+
 .form-wrapper.js-filter-wrapper {
   font-size: .75em;
 }
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index 84f10ba..3c2d105 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -40,8 +40,6 @@ libraries-extend:
     - dsfr4drupal/drupal.dialog
   core/drupal.dropbutton:
     - dsfr4drupal/drupal.dropbutton
-  core/drupal.form:
-    - dsfr4drupal/drupal.form
   core/drupal.message:
     - dsfr4drupal/drupal.message
   core/drupal.tabledrag:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index 7d080f5..dbb187b 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -148,6 +148,7 @@ component.form:
   css:
     component:
       /libraries/dsfr/dist/component/form/form.min.css: { minified: true }
+      css/component/form.css: {}
   dependencies:
     - dsfr4drupal/core
 
@@ -574,11 +575,6 @@ drupal.dropbutton:
     - core/drupal
     - core/once
 
-drupal.form:
-  css:
-    component:
-      css/component/form.css: {}
-
 drupal.layout_builder:
   css:
     theme:
diff --git a/templates/system/status-report-general-info.html.twig b/templates/system/status-report-general-info.html.twig
new file mode 100644
index 0000000..c1554f7
--- /dev/null
+++ b/templates/system/status-report-general-info.html.twig
@@ -0,0 +1,61 @@
+<h2>{{ 'General System Information'|t }}</h2>
+<div class="fr-grid-row fr-grid-row--gutters fr-mb-4v">
+  <div class="fr-col-12 fr-col-md-6">
+    <div class="system-status-general-info__item">
+      <h3 class="fr-h5 system-status-general-info__item-title">{{ 'Drupal Version'|t }}</h3>
+      {{ drupal.value }}
+      {% if drupal.description %}
+        {{ drupal.description }}
+      {% endif %}
+    </div>
+  </div>
+  <div class="fr-col-12 fr-col-md-6">
+    <div class="system-status-general-info__item">
+      <h3 class="fr-h5 system-status-general-info__item-title">{{ 'Web Server'|t }}</h3>
+      {{ webserver.value }}
+      {% if webserver.description %}
+        {{ webserver.description }}
+      {% endif %}
+    </div>
+  </div>
+  <div class="fr-col-12 fr-col-md-6">
+    <div class="system-status-general-info__item">
+      <h3 class="fr-h5 system-status-general-info__item-title">{{ 'PHP'|t }}</h3>
+      <h4 class="fr-h6">{{ 'Version'|t }}</h4> {{ php.value }}
+      {% if php.description %}
+        {{ php.description }}
+      {% endif %}
+
+      <h4 class="fr-h6">{{ 'Memory limit'|t }}</h4>{{ php_memory_limit.value }}
+      {% if php_memory_limit.description %}
+        {{ php_memory_limit.description }}
+      {% endif %}
+    </div>
+  </div>
+  <div class="fr-col-12 fr-col-md-6">
+    <div class="system-status-general-info__item">
+      <h3 class="fr-h5 system-status-general-info__item-title">{{ 'Database'|t }}</h3>
+      <h4 class="fr-h6">{{ 'Version'|t }}</h4>{{ database_system_version.value }}
+      {% if database_system_version.description %}
+        {{ database_system_version.description }}
+      {% endif %}
+
+      <h4 class="fr-h6">{{ 'System'|t }}</h4>{{ database_system.value }}
+      {% if database_system.description %}
+        {{ database_system.description }}
+      {% endif %}
+    </div>
+  </div>
+  <div class="fr-col-12">
+    <div class="system-status-general-info__item">
+      <h3 class="fr-h5 system-status-general-info__item-title">{{ 'Last Cron Run'|t }}</h3>
+      {{ cron.value }}
+      {% if cron.run_cron %}
+        {{ cron.run_cron }}
+      {% endif %}
+      {% if cron.description %}
+        {{ cron.description }}
+      {% endif %}
+    </div>
+  </div>
+</div>
-- 
GitLab


From ea149ee5cf2e54d94985fae6011e42a891f6e77c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 19 Mar 2025 17:43:28 +0100
Subject: [PATCH 39/45] Fix indent.

---
 src/Dsfr4DrupalPreRender.php | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/Dsfr4DrupalPreRender.php b/src/Dsfr4DrupalPreRender.php
index 0674233..cbea266 100644
--- a/src/Dsfr4DrupalPreRender.php
+++ b/src/Dsfr4DrupalPreRender.php
@@ -19,7 +19,7 @@ class Dsfr4DrupalPreRender implements TrustedCallbackInterface {
    *   The render array element.
    *
    * @return array
-   *     The new render array element.
+   *   The new render array element.
    */
   public static function horizontalTabs(array $element): array {
     return self::tabs($element, 'horizontal');
@@ -29,10 +29,10 @@ class Dsfr4DrupalPreRender implements TrustedCallbackInterface {
    * Prerender callback for Vertical Tabs element.
    *
    * @param array $element
-   *    The render array element.
+   *   The render array element.
    *
    * @return array
-   *    The new render array element.
+   *   The new render array element.
    */
   public static function verticalTabs(array $element): array {
     return self::tabs($element, 'vertical');
@@ -42,10 +42,10 @@ class Dsfr4DrupalPreRender implements TrustedCallbackInterface {
    * Prerender callback for tabs element.
    *
    * @param array $element
-   *    The render array element.
+   *   The render array element.
    *
    * @return array
-   *    The new render array element.
+   *   The new render array element.
    */
   private static function tabs(array $element, string $orientation): array {
     $isDetails = isset($element['group']['#type']) && $element['group']['#type'] === 'details';
-- 
GitLab


From 0e12abcf7154834140c01f3e7f2af6fcb92d0e72 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 19 Mar 2025 18:12:14 +0100
Subject: [PATCH 40/45] Stylize media grid page.

---
 css/theme/media-library.css | 35 ++++++++++++++++++++++++++++++-----
 dsfr4drupal.info.yml        |  2 ++
 dsfr4drupal.libraries.yml   |  9 ++++++++-
 includes/views.theme        | 31 +++++++++++++++++++++++++++++++
 js/media-library.view.js    | 24 ++++++++++++++++++++++++
 5 files changed, 95 insertions(+), 6 deletions(-)
 create mode 100644 js/media-library.view.js

diff --git a/css/theme/media-library.css b/css/theme/media-library.css
index 772f706..a31b7ac 100644
--- a/css/theme/media-library.css
+++ b/css/theme/media-library.css
@@ -8,14 +8,21 @@
 }
 
 /* Remove margin at the end of the form */
-.media-library-view .views-form {
+.js-media-library-view .views-form {
   margin-bottom: calc(var(--form-spacing) * -1);
 }
 
-.js-media-library-item {
+.js-media-library-view .views-row {
+  position: relative;
   margin-block: var(--form-item-spacing) !important; /* stylelint-disable-line declaration-no-important */
 }
 
+.js-media-library-view .views-row .fr-checkbox-group {
+  position: absolute;
+  margin-top: .5rem;
+  margin-left: .5rem;
+}
+
 .js-media-library-add-form-added-media {
   --ul-type: none;
 }
@@ -37,7 +44,25 @@
   font-weight: 700;
 }
 
-.views-field-media-library-select-form .fr-checkbox-group input[type="checkbox"] + label.visually-hidden {
-  margin-left: .5rem;
-  margin-top: .5rem;
+.media-library-select-all input[type="checkbox"] {
+  display: inline-block;
+  vertical-align: bottom;
+  width: 1.5rem;
+  height: 1.5rem;
+  margin-right: .5rem;
+}
+
+.media-library-item__edit,
+.media-library-item__remove {
+  position: absolute;
+  z-index: 1;
+  top: 1.25rem;
+}
+
+.media-library-item__edit {
+  right: 3.5rem;
+}
+
+.media-library-item__remove {
+  right: 1.25rem;
 }
diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml
index 3c2d105..caf13fb 100644
--- a/dsfr4drupal.info.yml
+++ b/dsfr4drupal.info.yml
@@ -33,6 +33,7 @@ ckeditor5-stylesheets:
   - css/ckeditor5.css
 
 libraries:
+  - dsfr4drupal/core
   - dsfr4drupal/utility
 
 libraries-extend:
@@ -52,6 +53,7 @@ libraries-extend:
     - dsfr4drupal/drupal.layout_builder
   media_library/view:
     - dsfr4drupal/media_library.theme
+    - dsfr4drupal/media_library.view
   media_library/widget:
     - dsfr4drupal/media_library.theme
   navigation/navigation.layout:
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index dbb187b..cb88047 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -121,6 +121,8 @@ component.display.button:
   css:
     theme:
       css/theme/display.button.css: {}
+  dependencies:
+    - dsfr4drupal/core
 
 component.connect:
   css:
@@ -650,8 +652,13 @@ media_library.theme:
   css:
     theme:
       css/theme/media-library.css: {}
+
+media_library.view:
+  js:
+     js/media-library.view.js: {}
   dependencies:
-    - dsfr4drupal/core
+    - core/drupal
+    - core/once
 
 navigation.layout:
   css:
diff --git a/includes/views.theme b/includes/views.theme
index 176e8f4..cd4484d 100644
--- a/includes/views.theme
+++ b/includes/views.theme
@@ -6,6 +6,7 @@
  */
 
 declare(strict_types=1);
+use Drupal\views\ViewExecutable;
 
 /**
  * Implements hook_preprocss_hook() for "views_view__media_library".
@@ -33,3 +34,33 @@ function dsfr4drupal_preprocess_views_view__media_library(array &$variables): vo
   $variables['#attached']['library'][] = 'dsfr4drupal/component.tab';
   $variables['#attached']['library'][] = 'dsfr4drupal/component.link';
 }
+
+/**
+ * Implements hook_views_pre_render().
+ */
+function dsfr4drupal_views_pre_render(ViewExecutable $view) {
+  if (
+    $view->id() === 'media_library' &&
+    $view->current_display === 'page'
+  ) {
+    $add_classes = function (&$option, array $classes_to_add) {
+      $classes = preg_split('/\s+/', $option);
+      $classes = array_filter($classes);
+      $classes = array_merge($classes, $classes_to_add);
+      $option = implode(' ', array_unique($classes));
+    };
+
+    if (array_key_exists('edit_media', $view->field)) {
+      $add_classes(
+        $view->field['edit_media']->options['alter']['link_class'],
+        ['media-library-item__edit', 'fr-btn', 'fr-btn--secondary', 'fr-btn--sm', 'fr-icon-ball-pen-fill']
+      );
+    }
+    if (array_key_exists('delete_media', $view->field)) {
+      $add_classes(
+        $view->field['delete_media']->options['alter']['link_class'],
+        ['media-library-item__remove', 'fr-btn', 'fr-btn--secondary', 'fr-btn--sm', 'fr-icon-close-line']
+      );
+    }
+  }
+}
diff --git a/js/media-library.view.js b/js/media-library.view.js
new file mode 100644
index 0000000..e360d7d
--- /dev/null
+++ b/js/media-library.view.js
@@ -0,0 +1,24 @@
+/**
+ * @file
+ * Manage media library view behaviors.
+ */
+
+((Drupal, once) => {
+  "use strict";
+
+
+  Drupal.behaviors.dsfrMediaLibraryView = {
+    attach: (context) => {
+      const selectAll =  once("dsfr-media-library-select-all", ".media-library-select-all", context);
+      if (!selectAll.length) {
+        return;
+      }
+
+      selectAll.forEach(element => {
+        // Move "select all" element after header.
+        context.querySelector("#edit-header").after(element);
+      });
+    },
+  };
+
+})(Drupal, once);
-- 
GitLab


From 66650cd0d81b54cb75a86f11121423b7e86a3416 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Wed, 19 Mar 2025 20:07:03 +0100
Subject: [PATCH 41/45] Fix linter.

---
 css/component/dropbutton.css |  1 +
 css/component/form.css       | 60 ++++++++++++++++++------------------
 2 files changed, 31 insertions(+), 30 deletions(-)

diff --git a/css/component/dropbutton.css b/css/component/dropbutton.css
index eb147fd..fbf4881 100644
--- a/css/component/dropbutton.css
+++ b/css/component/dropbutton.css
@@ -21,6 +21,7 @@ html.js .dropbutton-wrapper .dropbutton .secondary-action {
   padding: .25rem;
   background-color: var(--background-default-grey);
 }
+
 .dropbutton__items a {
   margin-right: 2em;
 }
diff --git a/css/component/form.css b/css/component/form.css
index a94449c..e8222dd 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -26,7 +26,35 @@
   --form-spacing: 0;
 }
 
-/*Remove specific spacing into fieldset - START */
+.fr-checkbox-group + .fr-checkbox-group,
+.fr-radio-group + .fr-radio-group {
+  margin-top:  .25rem;
+}
+
+.form-actions a + button,
+.form-actions button + button,
+.form-actions button + a {
+  margin-left: var(--form-item-spacing);
+}
+
+.fr-label > .field-edit-link {
+  font-size: .75em;
+}
+
+.fr-label .field-edit-link > button {
+  padding: 0;
+}
+
+.fr-upload-group {
+  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing);
+  border: 1px solid var(--border-default-grey);
+}
+
+.fr-upload-group > label {
+  font-weight: 700;
+}
+
+/* Remove specific spacing into fieldset - START */
 .fr-fieldset__content .fr-checkbox-group input[type="checkbox"] + label::before,
 .fr-fieldset__content .fr-radio-group:not(.fr-radio-rich) input[type="radio"] + label::before {
   top: auto;
@@ -36,7 +64,7 @@
 .fr-fieldset__content .fr-radio-group label {
   padding: 0;
 }
-/*Remove specific spacing into fieldset - END */
+/* Remove specific spacing into fieldset - END */
 
 main[role="main"] > .fr-container > form,
 main[role="main"] > .fr-container--fluid > form {
@@ -47,21 +75,10 @@ main[role="main"] > .fr-container--fluid > form {
   margin-block: var(--form-item-spacing);
 }
 
-.fr-checkbox-group + .fr-checkbox-group,
-.fr-radio-group + .fr-radio-group {
-  margin-top:  .25rem;
-}
-
 .form-wrapper.js-filter-wrapper {
   font-size: .75em;
 }
 
-.form-actions a + button,
-.form-actions button + button,
-.form-actions button + a {
-  margin-left: var(--form-item-spacing);
-}
-
 /* Disable table scrolling into form item - START */
 .form-item .fr-table__container {
   overflow: initial;
@@ -77,23 +94,6 @@ main[role="main"] > .fr-container--fluid > form {
 }
 /* Disable table scrolling into form - END */
 
-.fr-label > .field-edit-link {
-  font-size: .75em;
-}
-
-.fr-label .field-edit-link > button {
-  padding: 0;
-}
-
-.fr-upload-group {
-  padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing);
-  border: 1px solid var(--border-default-grey);
-}
-
-.fr-upload-group > label {
-  font-weight: 700;
-}
-
 .js-filter-wrapper {
   margin-top: calc(var(--form-item-spacing) * -.75);
 }
-- 
GitLab


From 19d2bb1773ad9fdd1a3c348a0b4037c24179163b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Thu, 20 Mar 2025 14:45:01 +0100
Subject: [PATCH 42/45] Limit views exposed form styles to admin pages and some
 other views.

---
 css/component/form.css               |  6 ++++++
 css/component/views-exposed-form.css |  2 --
 includes/form.theme                  | 22 ++++++++++++++++++++++
 3 files changed, 28 insertions(+), 2 deletions(-)

diff --git a/css/component/form.css b/css/component/form.css
index e8222dd..46e53f3 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -26,6 +26,12 @@
   --form-spacing: 0;
 }
 
+/* Provide a class to wrap form with borders. */
+.dsfr4drupal-form-bordered {
+  padding: var(--form-spacing-xs) var(--form-spacing-s) var(--form-spacing-s);
+  border: 1px solid var(--border-default-grey);
+}
+
 .fr-checkbox-group + .fr-checkbox-group,
 .fr-radio-group + .fr-radio-group {
   margin-top:  .25rem;
diff --git a/css/component/views-exposed-form.css b/css/component/views-exposed-form.css
index 3ebbefc..fa7ba22 100644
--- a/css/component/views-exposed-form.css
+++ b/css/component/views-exposed-form.css
@@ -10,8 +10,6 @@
   display: flex;
   flex-wrap: wrap;
   margin-block: var(--form-spacing-s);
-  padding: var(--form-spacing-xs) var(--form-spacing-s) var(--form-spacing-s);
-  border: 1px solid var(--border-default-grey);
 }
 
 .views-exposed-form--preview.views-exposed-form--preview {
diff --git a/includes/form.theme b/includes/form.theme
index db0eeb8..151c71a 100644
--- a/includes/form.theme
+++ b/includes/form.theme
@@ -34,6 +34,28 @@ function dsfr4drupal_form_node_preview_form_select_alter(array &$form, FormState
   $form['#attached']['library'][] = 'dsfr4drupal/component.link';
 }
 
+/**
+ * Implements hook_form_FORM_ID_alter() for "views_exposed_form".
+ */
+function dsfr4drupal_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
+  /** @var \Drupal\views\ViewExecutable $view */
+  $view = $form_state->getStorage()['view'];
+
+  $view_ids = [
+    // Stylize media library widget modal.
+    'media_library',
+  ];
+
+  if (
+    // Stylize views exposed form on admin pages.
+    \Drupal::service('router.admin_context')->isAdminRoute() ||
+    // Specific rule for some views.
+    in_array($view->id(), $view_ids)
+  ) {
+    $form['#attributes']['class'][] = 'dsfr4drupal-form-bordered';
+  }
+}
+
 /**
  * Implements hook_preprocess_HOOK() for "fieldset".
  */
-- 
GitLab


From 57e5ac78f326370ac49f7e10661ac09202b156ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Thu, 20 Mar 2025 15:45:53 +0100
Subject: [PATCH 43/45] Finalize rendering.

---
 css/component/form.css                           |  6 +++---
 css/component/radio.css                          | 12 ++++++++++++
 css/component/views-exposed-form.css             | 16 ++++++++--------
 dsfr4drupal.libraries.yml                        |  1 +
 includes/form.theme                              |  3 ++-
 .../block/block--local-actions-block.html.twig   |  2 +-
 6 files changed, 27 insertions(+), 13 deletions(-)
 create mode 100644 css/component/radio.css

diff --git a/css/component/form.css b/css/component/form.css
index 46e53f3..e8c9651 100644
--- a/css/component/form.css
+++ b/css/component/form.css
@@ -27,7 +27,7 @@
 }
 
 /* Provide a class to wrap form with borders. */
-.dsfr4drupal-form-bordered {
+.dsfr4drupal-form--bordered {
   padding: var(--form-spacing-xs) var(--form-spacing-s) var(--form-spacing-s);
   border: 1px solid var(--border-default-grey);
 }
@@ -77,8 +77,8 @@ main[role="main"] > .fr-container--fluid > form {
   margin-block: var(--form-spacing);
 }
 
-.form-wrapper {
-  margin-block: var(--form-item-spacing);
+.form-wrapper:not(:last-child) {
+  margin-bottom: var(--form-item-spacing);
 }
 
 .form-wrapper.js-filter-wrapper {
diff --git a/css/component/radio.css b/css/component/radio.css
new file mode 100644
index 0000000..ffc4840
--- /dev/null
+++ b/css/component/radio.css
@@ -0,0 +1,12 @@
+/**
+ * @file
+ * Manage styles for radio.
+ */
+
+.fr-fieldset .fr-fieldset__content .fr-radio-group:not(.fr-radio-rich) input[type="radio"] {
+  top: auto;
+}
+
+.fr-fieldset .fr-fieldset__content .fr-radio-group:not(.fr-radio-rich) input[type="radio"] + label {
+  background-position: left center;
+}
diff --git a/css/component/views-exposed-form.css b/css/component/views-exposed-form.css
index fa7ba22..263b11a 100644
--- a/css/component/views-exposed-form.css
+++ b/css/component/views-exposed-form.css
@@ -6,7 +6,7 @@
 /**
  * Use flexbox and some margin resets to make the fields + actions go inline.
  */
-.views-exposed-form {
+.views-exposed-form--inline {
   display: flex;
   flex-wrap: wrap;
   margin-block: var(--form-spacing-s);
@@ -16,24 +16,24 @@
   margin-top: 0;
 }
 
-.views-exposed-form__item {
+.views-exposed-form--inline .views-exposed-form__item {
   max-width: 100%;
   margin-block: var(--form-spacing-s) 0;
   margin-inline: 0 var(--form-spacing-xs);
 }
 
-.views-exposed-form .form-item--no-label,
-.views-exposed-form__item.views-exposed-form__item.views-exposed-form__item--actions {
+.views-exposed-form--inline .form-item--no-label,
+.views-exposed-form--inline .views-exposed-form__item.views-exposed-form__item.views-exposed-form__item--actions {
   margin-block: var(--form-spacing-s) 0;
   align-self: flex-end;
 }
 
-.views-exposed-form .form-item--no-label,
-.views-exposed-form__item.views-exposed-form__item--actions {
+.views-exposed-form--inline .form-item--no-label,
+.views-exposed-form--inline .views-exposed-form__item.views-exposed-form__item--actions {
   margin-top: calc(var(--form-label-line-height) + var(--form-label-input-spacing));
 }
 
-.views-exposed-form .fr-input-group:not(:last-child),
-.views-exposed-form .fr-select-group:not(:last-child) {
+.views-exposed-form--inline .fr-input-group:not(:last-child),
+.views-exposed-form--inline .fr-select-group:not(:last-child) {
   margin-bottom: 0;
 }
diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml
index cb88047..57f6416 100644
--- a/dsfr4drupal.libraries.yml
+++ b/dsfr4drupal.libraries.yml
@@ -313,6 +313,7 @@ component.radio:
   css:
     component:
       /libraries/dsfr/dist/component/radio/radio.min.css: { minified: true }
+      css/component/radio.css: {}
   dependencies:
     - dsfr4drupal/component.form
     - dsfr4drupal/core
diff --git a/includes/form.theme b/includes/form.theme
index 151c71a..f7582d4 100644
--- a/includes/form.theme
+++ b/includes/form.theme
@@ -52,7 +52,8 @@ function dsfr4drupal_form_views_exposed_form_alter(array &$form, FormStateInterf
     // Specific rule for some views.
     in_array($view->id(), $view_ids)
   ) {
-    $form['#attributes']['class'][] = 'dsfr4drupal-form-bordered';
+    $form['#attributes']['class'][] = 'views-exposed-form--inline';
+    $form['#attributes']['class'][] = 'dsfr4drupal-form--bordered';
   }
 }
 
diff --git a/templates/block/block--local-actions-block.html.twig b/templates/block/block--local-actions-block.html.twig
index c117571..04c8bbe 100644
--- a/templates/block/block--local-actions-block.html.twig
+++ b/templates/block/block--local-actions-block.html.twig
@@ -2,7 +2,7 @@
 
 {% block content %}
   {% if content %}
-    <ul class="local-actions">
+    <ul class="local-actions fr-mb-4v">
       {{ content }}
     </ul>
     {{ attach_library('dsfr4drupal/local-actions') }}
-- 
GitLab


From 7c61f8bdff8988f4855eb89d1977f7a789c8b7e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Thu, 20 Mar 2025 15:49:00 +0100
Subject: [PATCH 44/45] Manage help text as tooltip in fieldset.

---
 includes/form.theme                         | 3 +++
 templates/form/fieldset.html.twig           | 9 ++++++++-
 templates/form/form-element-label.html.twig | 3 +--
 3 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/includes/form.theme b/includes/form.theme
index f7582d4..7f73ee6 100644
--- a/includes/form.theme
+++ b/includes/form.theme
@@ -75,6 +75,9 @@ function dsfr4drupal_preprocess_fieldset(array &$variables): void {
     $variables['attributes']['aria-labelledby'] .= ' ' . $variables['error_id'];
   }
 
+  // Show help text as tooltip.
+  $variables['description_tooltip'] = theme_get_setting('form_description_tooltip') ?? FALSE;
+
   $variables['#attached']['library'][] = 'dsfr4drupal/component.form';
 }
 
diff --git a/templates/form/fieldset.html.twig b/templates/form/fieldset.html.twig
index 2143e15..a7bdb64 100644
--- a/templates/form/fieldset.html.twig
+++ b/templates/form/fieldset.html.twig
@@ -14,7 +14,14 @@
   <fieldset{{ attributes.setAttribute('role', 'group').addClass(classes) }}>
     <legend{{ legend.attributes.addClass(legend_classes) }}>{{ legend.title }}
       {% if description.content %}
-        <span class="fr-hint-text">{{ description.content }}</span>
+        {% if description_tooltip %}
+          {{ include('dsfr4drupal:tooltip', {
+            'title': tooltip_title|default('Help text'|t),
+            'tooltip': description.content,
+          }, with_context=false) }}
+        {% else %}
+          <span class="fr-hint-text">{{ description.content }}</span>
+        {% endif %}
       {% endif %}
     </legend>
     <div class="fr-fieldset__content">
diff --git a/templates/form/form-element-label.html.twig b/templates/form/form-element-label.html.twig
index a8138ad..0686a5c 100644
--- a/templates/form/form-element-label.html.twig
+++ b/templates/form/form-element-label.html.twig
@@ -1,4 +1,3 @@
-{% set tooltip_title = tooltip_title|default('Help text'|t) %}
 {% set classes = [
   title_display == 'after' ? 'option',
   title_display == 'invisible' ? 'visually-hidden',
@@ -11,7 +10,7 @@
     {% if description.content %}
       {% if description_tooltip %}
         {{ include('dsfr4drupal:tooltip', {
-          'title': tooltip_title,
+          'title': tooltip_title|default('Help text'|t),
           'tooltip': description.content,
         }, with_context=false) }}
       {% else %}
-- 
GitLab


From e1b90b9845eb1d0334c54fa61ff5d5f76e1d79fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20BRINDLE?= <sebastien.brindle@kleegroup.com>
Date: Fri, 21 Mar 2025 12:04:56 +0100
Subject: [PATCH 45/45] Remove useless "role" attribute.

---
 templates/form/fieldset.html.twig | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/form/fieldset.html.twig b/templates/form/fieldset.html.twig
index a7bdb64..1584727 100644
--- a/templates/form/fieldset.html.twig
+++ b/templates/form/fieldset.html.twig
@@ -11,7 +11,7 @@
   required ? 'form-required',
 ] %}
 <div class="fr-form-group">
-  <fieldset{{ attributes.setAttribute('role', 'group').addClass(classes) }}>
+  <fieldset{{ attributes.addClass(classes) }}>
     <legend{{ legend.attributes.addClass(legend_classes) }}>{{ legend.title }}
       {% if description.content %}
         {% if description_tooltip %}
-- 
GitLab