diff --git a/FEATURES/137374 b/FEATURES/137374
new file mode 100644
index 0000000000000000000000000000000000000000..655ce1f8a8932b3f69bee6a22d9097bbc21de1ac
--- /dev/null
+++ b/FEATURES/137374
@@ -0,0 +1,10 @@
+        '137374' =>
+            ['Label' => $this->_('Parcourir l\'étagère'),
+             'Desc' => $this->_('Dans les exemplaires, un carrousel de notices représentant les documents sur l\'étagère dans la bibliothèque est disponible à l\'affichage.'),
+             'Image' => '',
+             'Video' => 'https://youtu.be/WCjz4bFGWeA',
+             'Category' => $this->_('Recherche'),
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => 'https://wiki.bokeh-library-portal.org/index.php?title=Navigation_par_%C3%A9tag%C3%A8re',
+             'Test' => '',
+             'Date' => '2022-05-10'],
\ No newline at end of file
diff --git a/UPGRADE.fr.md b/UPGRADE.fr.md
index 98c6c79a0ccdc4ac9a5993f7ffad5fcb00777a79..5261ae89b49b3faa184e71be0b8f15065308e865 100644
--- a/UPGRADE.fr.md
+++ b/UPGRADE.fr.md
@@ -120,4 +120,6 @@ vous devez procéder à l'étape d'installation de chacune d'elle.
  
  - 8.0.159 - 31/05/2020 : Suite à une amélioration de l'ergonomie, le résultat de recherche
    	     		  et la liste des exemplaires dans le magasin de thèmes passent de
-			  l'affichage mur à l'affichage grille.
\ No newline at end of file
+			  l'affichage mur à l'affichage grille.
+
+ - 8.0.160 - 20/06/2022 : cosmogramme/sql/patch/patch_433.php
\ No newline at end of file
diff --git a/VERSIONS_WIP/137374 b/VERSIONS_WIP/137374
new file mode 100644
index 0000000000000000000000000000000000000000..f5c1a4ab4b41c8d38a6f9f30379731b64beede02
--- /dev/null
+++ b/VERSIONS_WIP/137374
@@ -0,0 +1 @@
+ - fonctionnalité #137374 : Exemplaires : Un carrousel de notices représentant les documents sur l'étagère de la bibliothèque est maintenant disponible. L'affichage de ce dernier se configure dans les paramètres de l'affichage des exemplaires.
\ No newline at end of file
diff --git a/application/modules/opac/controllers/RechercheController.php b/application/modules/opac/controllers/RechercheController.php
index 71f7d6fc80909d57ec87f11f8af0945a4543fdd2..166d9863bae0bf1f69e15a2b363a6bc2c1312aa8 100644
--- a/application/modules/opac/controllers/RechercheController.php
+++ b/application/modules/opac/controllers/RechercheController.php
@@ -83,6 +83,18 @@ class RechercheController extends ZendAfi_Controller_Action {
   }
 
 
+  public function shelfAction() {
+    if (!($item = Class_Exemplaire::find($this->_getParam('item_id'))))
+      throw new Zend_Controller_Action_Exception($this->view->_('Exemplaire non trouvé'), 404);
+
+    $this->view->content = $this->view->itemShelf(new Class_Exemplaire_Shelf($item));
+
+    if ('ajax' === $this->_getParam('render'))
+      $this->_helper->getHelper('HTMLAjaxResponse')
+                    ->htmlAjaxResponseWithScript($this->view->content);
+  }
+
+
   public function saisieAction() {
     $this->view->expressionRecherche = $this->_request->getParam('expressionRecherche');
   }
diff --git a/application/modules/opac/views/scripts/recherche/shelf.phtml b/application/modules/opac/views/scripts/recherche/shelf.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..ff71d31e883048cb05ea7b632370dd47b1fe1517
--- /dev/null
+++ b/application/modules/opac/views/scripts/recherche/shelf.phtml
@@ -0,0 +1,2 @@
+<?php
+echo $this->content;
diff --git a/cosmogramme/php/classes/classe_notice_integration.php b/cosmogramme/php/classes/classe_notice_integration.php
index 639069e8246009f24e52354f34e67cd9399e2d5d..bdf5b1ee9b02662689bad233adce111ff02540a4 100644
--- a/cosmogramme/php/classes/classe_notice_integration.php
+++ b/cosmogramme/php/classes/classe_notice_integration.php
@@ -28,7 +28,9 @@ require_once 'classe_profil_donnees.php';
 require_once 'classe_communication.php';
 
 class notice_integration {
-  use Trait_CodifProviderAware;
+  use
+    Trait_CodifProviderAware,
+    Trait_TimeSource;
 
   const
     RECORD_REJECT = 0,
@@ -80,12 +82,11 @@ class notice_integration {
     $_codification_rules,
     $_raw_data;
 
-
   public function __construct() {
     $this->indexation = indexation::getInstance();
     $this->filtrer_fulltext = Class_CosmoVar::get("filtrer_fulltext");
     $this->mode_doublon = Class_CosmoVar::get("mode_doublon");
-    $this->notice_sgbd = new notice_unimarc();
+    $this->notice_sgbd = new notice_unimarc;
   }
 
 
@@ -814,7 +815,9 @@ class notice_integration {
     $this->_deleteAllExemplairesWithSubfield($exemplaires, $id_notice);
 
     foreach($exemplaires as $exemplaire)
-      $exemplaire->save();
+      ($exemplaire
+       ->setShelfKeyUpdateDate($this->getCurrentDateTime())
+       ->save());
 
     (new Class_Cosmogramme_Integration_RawRecord($this->_raw_data,
                                                  $this->notice['id_origine'],
diff --git a/cosmogramme/php/integre_traite_main.php b/cosmogramme/php/integre_traite_main.php
index fb7c08a8c2e852a9c0d9020bff6c77177f675fee..8c2c9cce0b13f35c8a20562cd2c3d31777c329e0 100644
--- a/cosmogramme/php/integre_traite_main.php
+++ b/cosmogramme/php/integre_traite_main.php
@@ -257,6 +257,12 @@ if (!$should_skip_records) {
   // Facets on domains (phase 7.2)
   // ----------------------------------------------------------------
   startIntegrationPhase('DynamicFacetsOnDomainIndex');
+
+
+  // ----------------------------------------------------------------
+  // Items Shelf Key generation (phase 7.3)
+  // ----------------------------------------------------------------
+  startIntegrationPhase('ItemsShelfKeyGeneration');
 }
 
 $phase = 7.5;
diff --git a/cosmogramme/sql/patch/patch_434.php b/cosmogramme/sql/patch/patch_434.php
new file mode 100644
index 0000000000000000000000000000000000000000..3cc7708c8bdc0a8903f66a24a589d5cba645310b
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_434.php
@@ -0,0 +1,6 @@
+<?php
+$adapter = Zend_Db_Table_Abstract::getDefaultAdapter();
+
+try {
+  $adapter->query('ALTER TABLE exemplaires ADD COLUMN shelf_key VARCHAR(255) NULL DEFAULT NULL, ADD COLUMN shelf_key_update_date TIMESTAMP NULL DEFAULT NULL');
+} catch(Exception $e) {}
diff --git a/cosmogramme/tests/php/classes/NoticeIntegrationTest.php b/cosmogramme/tests/php/classes/NoticeIntegrationTest.php
index 261de58c70bdc1bb6f0a5b3c2320fd3e41e25421..29006511fddb39030f3f1a21592eded5a4e64793 100644
--- a/cosmogramme/tests/php/classes/NoticeIntegrationTest.php
+++ b/cosmogramme/tests/php/classes/NoticeIntegrationTest.php
@@ -23,6 +23,7 @@ require_once 'classe_notice_integration.php';
 require_once 'classe_notice_marc21.php';
 require_once 'classe_codif_cache.php';
 require_once 'ModelTestCase.php';
+require_once 'tests/library/Class/TimeSourceForTest.php';
 
 
 abstract class NoticeIntegrationTestCase extends ModelTestCase {
@@ -1451,3 +1452,26 @@ class NoticeIntegrationAsservissementConsentiTest extends NoticeIntegrationTestC
                         $this->notice_data['titre_princ']);
   }
 }
+
+
+
+
+class NoticeIntegrationItemsUpdateDateTest extends NoticeIntegrationTestCase {
+  public function setUp() {
+    parent::setUp();
+    notice_integration::setTimeSource(new TimeSourceForTest('2022-06-20 16:53:17'));
+    $this->loadNotice("unimarc_musso");
+  }
+
+
+  public function tearDown() {
+    notice_integration::setTimeSource(null);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function itemsShouldHaveShelfKeyUpdateDateSetTo20220620() {
+    $this->assertEquals(8, Class_Exemplaire::countBy(['shelf_key_update_date' => '2022-06-20 16:53:17']));
+  }
+}
\ No newline at end of file
diff --git a/library/Class/Cosmogramme/Integration/Chronometre.php b/library/Class/Cosmogramme/Integration/Chronometre.php
index 01ab883f196c6c627fae7e990f41b0ee28c09981..a0094279b18203269b931ba96d53ad057d4d279a 100644
--- a/library/Class/Cosmogramme/Integration/Chronometre.php
+++ b/library/Class/Cosmogramme/Integration/Chronometre.php
@@ -118,6 +118,11 @@ class Class_Cosmogramme_Integration_Chronometre {
   }
 
 
+  public function elapsedOnFile() {
+    return $this->_on_file->elapsed();
+  }
+
+
   public function mainAverage($number, $label) {
     return $this->_main->average($number, $label);
   }
diff --git a/library/Class/Cosmogramme/Integration/PhaseAbstract.php b/library/Class/Cosmogramme/Integration/PhaseAbstract.php
index 0928e7c2a1dcae65221e44d461f224a29ce88d2a..22b301e31fcbc32fcfba88735efd044182dcd1e7 100644
--- a/library/Class/Cosmogramme/Integration/PhaseAbstract.php
+++ b/library/Class/Cosmogramme/Integration/PhaseAbstract.php
@@ -21,8 +21,11 @@
 
 
 abstract class Class_Cosmogramme_Integration_PhaseAbstract {
+
   use Trait_TimeSource, Trait_StaticFileSystem, Trait_Translator, Trait_MemoryCleaner;
 
+  protected static $_should_throw_error = false;
+
   protected $_label = '',
     $_phase,
     $_log,
@@ -92,6 +95,9 @@ abstract class Class_Cosmogramme_Integration_PhaseAbstract {
 
     $this->_printLabel();
 
+    if (static::$_should_throw_error)
+      return $this->_execute();
+
     try {
       $this->_execute();
     } catch (Exception | Throwable $e) {
@@ -217,4 +223,9 @@ abstract class Class_Cosmogramme_Integration_PhaseAbstract {
 
     return $this;
   }
+
+
+  public static function shouldThrowError($should) {
+    static::$_should_throw_error = $should;
+  }
 }
diff --git a/library/Class/Cosmogramme/Integration/PhaseItemsShelfKeyGeneration.php b/library/Class/Cosmogramme/Integration/PhaseItemsShelfKeyGeneration.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d58321b34e76e4ac9a2d007ccdfd0074410c129
--- /dev/null
+++ b/library/Class/Cosmogramme/Integration/PhaseItemsShelfKeyGeneration.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Cosmogramme_Integration_PhaseItemsShelfKeyGeneration
+  extends Class_Cosmogramme_Integration_PhaseAbstract {
+
+  const
+    MY_ID = 7.3,
+    MAX_ITEMS = 1000;
+
+
+  public function __construct($phase, $log, $chrono) {
+    parent::__construct($phase, $log, $chrono);
+    $this->_label = $this->_('Mise à jour des clés d\'étagère des exemplaires');
+  }
+
+
+  protected function _init($phase) {}
+
+
+  protected function _previousPhaseIds() : array {
+    return [7.2];
+  }
+
+
+  public function _execute() {
+    (new Class_Migration_IndexItemsShelfKey)
+      ->setEchoFunction(fn($message) => $this->_logMessage($message))
+      ->run();
+
+    return $this->_phase;
+  }
+}
diff --git a/library/Class/Exemplaire.php b/library/Class/Exemplaire.php
index 47e125ed48dfca09869cc4916839c3f989e23570..ec39cb870ead4e0bbb038f6f0ba5faff03e2fb4c 100644
--- a/library/Class/Exemplaire.php
+++ b/library/Class/Exemplaire.php
@@ -147,7 +147,9 @@ class Class_Exemplaire extends Storm_Model_Abstract {
                                           'id_int_bib' => 0,
                                           'id_data_profile' => 0,
                                           'type' => Class_Notice::TYPE_BIBLIOGRAPHIC,
-                                          'cote' => ''];
+                                          'cote' => '',
+                                          'shelf_key' => '',
+                                          'shelf_key_update_date' => '0000-00-00 00:00:00'];
 
   protected
     $_sigb_exemplaire;
@@ -269,6 +271,20 @@ class Class_Exemplaire extends Storm_Model_Abstract {
   }
 
 
+  public function getClefChapeau() : string {
+    return ($record = $this->getNotice())
+      ? $record->getClefChapeau()
+      : '';
+  }
+
+
+  public function getClefAlpha() : string {
+    return ($record = $this->getNotice())
+      ? $record->getClefAlpha()
+      : '';
+  }
+
+
   public function getILSWsItem() : Class_WebService_SIGB_Exemplaire {
     if ( $this->_sigb_exemplaire)
       return $this->_sigb_exemplaire;
@@ -706,4 +722,11 @@ class Class_Exemplaire extends Storm_Model_Abstract {
   public function isCalendarHoldRequired() : bool {
     return (bool) $this->_withILSWsItemDo(fn($ils_ws_item) => $ils_ws_item->requiresCalendarHold());
   }
+
+
+  public function initShelfKey() : self {
+    return $this->hasCote()
+      ? $this->setShelfKey((new Class_Exemplaire_ShelfKey)->generateForItem($this))
+      : $this;
+  }
 }
diff --git a/library/Class/Exemplaire/Shelf.php b/library/Class/Exemplaire/Shelf.php
new file mode 100644
index 0000000000000000000000000000000000000000..80811344e68e4616ca4e15abc87b396b5fd492e5
--- /dev/null
+++ b/library/Class/Exemplaire/Shelf.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Exemplaire_Shelf {
+
+  protected Class_Exemplaire $_item;
+
+  public function __construct(Class_Exemplaire $item) {
+    $this->_item = $item;
+  }
+
+
+  public function getBibLibelle() : string {
+    return $this->_item->getBibLibelle();
+  }
+
+
+  public function renderOn(Callable $callback) : string {
+    return $callback($this->_findBeforeItems(),
+                     $this->_item,
+                     $this->_findAfterItems());
+  }
+
+
+  protected function _findBeforeItems() : array {
+    return array_reverse($this->_findItems(Class_Exemplaire::clauseLesser('shelf_key',
+                                                                          $this->_item->getShelfKey()),
+                                           'shelf_key desc'));
+  }
+
+
+  protected function _findAfterItems() : array {
+    return $this->_findItems(Class_Exemplaire::clauseGreater('shelf_key',
+                                                             $this->_item->getShelfKey()),
+                             'shelf_key');
+  }
+
+
+  protected function _findItems(Storm_Model_PersistenceStrategy_Clause $clause,
+                                string $order) : array {
+    return Class_Exemplaire::findAllBy([$clause,
+                                        'order' => $order,
+                                        'id_bib' => $this->_item->getIdBib(),
+                                        'section' => $this->_item->getSection(),
+                                        'emplacement' => $this->_item->getEmplacement(),
+                                        'id_notice not' => $this->_item->getIdNotice(),
+                                        'limit' => 12,
+                                        'group_by' => 'id_notice']);
+  }
+}
diff --git a/library/Class/Exemplaire/ShelfKey.php b/library/Class/Exemplaire/ShelfKey.php
new file mode 100644
index 0000000000000000000000000000000000000000..f8e768a7b2b07c513f50cf58cc0e094c76556f83
--- /dev/null
+++ b/library/Class/Exemplaire/ShelfKey.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Exemplaire_ShelfKey {
+
+  protected int $_subkey_len = 4;
+  protected int $_keyparts_count = 6;
+
+  /**
+   * Generate a key to order items on a virtual shelf
+   *
+   * Example:
+   * for item 'id' => 111, 'cote' => 'AMT R GER 740.1 698', ''
+   * and Record 'tome_alpha' => '365', 'clef_alpha' => 'OTHERTITLE', 'clef_chapeau' => 'KEYCHAPEAU',
+   * => 0AMT_000R_0GER_0740_0001_0698_KEYCHAPE_0365_OTHERTIT_0111
+   */
+  public function generateForItem(Class_Exemplaire $item) : string {
+    return $this->generateForArray(['cote' => $item->getCote(),
+                                    'clef_chapeau' => $item->getClefChapeau(),
+                                    'tome_alpha' => $item->getTomeAlpha(),
+                                    'clef_alpha' => $item->getClefAlpha(),
+                                    'id' => $item->getId()]);
+  }
+
+
+  public function generateForArray(array $row) : string {
+    $parts = $this->_coteKeyParts($row['cote']);
+    $parts [] = $this->_padLeft($row['clef_chapeau'], $this->_subkey_len * 2);
+    $parts [] = $this->_padLeft($row['tome_alpha'], $this->_subkey_len);
+    $parts [] = $this->_padLeft($row['clef_alpha'], $this->_subkey_len * 2);
+    $parts [] = str_pad(substr($row['id'],
+                               -$this->_subkey_len),
+                        $this->_subkey_len,
+                        '0',
+                        STR_PAD_LEFT);
+
+    return implode('_', $parts);
+  }
+
+
+  protected function _padLeft(string $part, int $length) : string {
+    return str_pad(substr($part, 0, $length),
+                   $length,
+                   '0',
+                   STR_PAD_LEFT);
+  }
+
+
+  protected function _coteKeyParts(string $cote) : array {
+    return array_map(fn($key) => $this->_padLeft($key, $this->_subkey_len),
+                     array_pad(array_slice(explode(' ',
+                                                   Class_Indexation::getInstance()->alphaMaj($cote)),
+                                           0,
+                                           $this->_keyparts_count),
+                               $this->_keyparts_count,
+                               ''));
+  }
+}
\ No newline at end of file
diff --git a/library/Class/Indexation/PseudoNotice.php b/library/Class/Indexation/PseudoNotice.php
index 3585a9835aa1cdbb4beb01d1facdaebf0e39bbac..8c7898433b2b4b4081186ec584d17b8cb1339e4a 100644
--- a/library/Class/Indexation/PseudoNotice.php
+++ b/library/Class/Indexation/PseudoNotice.php
@@ -166,9 +166,10 @@ class Class_Indexation_PseudoNotice {
     }
 
     $this->_exemplaire = Class_Exemplaire::newInstance(['id_bib' => $this->_datas['id_bib'],
-                                                        'id_notice' => $this->_notice->getId(),
+                                                        'notice' => $this->_notice,
                                                         'id_origine' => $this->_model->getId(),
                                                         'activite' => $this->_('A consulter sur le portail')]);
+
     if ($this->_exemplaire->save()) {
       $this->_notice->addExemplaire($this->_exemplaire);
       return true;
diff --git a/library/Class/Migration/IndexItemsShelfKey.php b/library/Class/Migration/IndexItemsShelfKey.php
new file mode 100644
index 0000000000000000000000000000000000000000..62c8069bb77423c87ff98a90646e91ca5ecc5082
--- /dev/null
+++ b/library/Class/Migration/IndexItemsShelfKey.php
@@ -0,0 +1,147 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Migration_IndexItemsShelfKey {
+
+  use
+    Trait_TimeSource,
+    Trait_EchoError,
+    Trait_Translator,
+    Trait_MemoryCleaner;
+
+
+  protected Class_Exemplaire_ShelfKey $_shelf_key_generator;
+  protected Class_Cosmogramme_Integration_Chronometre $_chrono;
+  protected string $_current_date_time = '';
+  protected int $_page_size = 5000;
+
+
+  public function run() : void {
+    $this->_chrono = (new Class_Cosmogramme_Integration_Chronometre)->startMain();
+
+    $this->echoStartTitle($this->_('Début de la phase d\'indexation des clés d\'étagères des exemplaires'));
+
+    $this->_shelf_key_generator = new Class_Exemplaire_ShelfKey;
+    $this->_current_date_time = $this->getCurrentDateTime();
+    $this->_sql = Zend_Registry::get('sql');
+
+    $this->_run();
+
+    Class_CosmoVar::setValueOf('shelf_key_update_date', $this->_current_date_time);
+
+    $this->echoEndTitle($this->_('Fin de la phase d\'indexation des clés d\'étagères. Durée : %ss' ,
+                                 $this->_chrono->mainElapsed()));
+  }
+
+
+  protected function _run() : self {
+    $this->_chrono->startOnFile();
+    $title = $this->_('Indexation des clés');
+    $this->echoStartTitle($title);
+
+    $where_clause =
+      sprintf('exemplaires.cote > \'\' AND '
+              . '( exemplaires.shelf_key IS NULL '
+              . 'OR exemplaires.shelf_key = \'\' '
+              . 'OR ( exemplaires.shelf_key_update_date > \'%s\' '
+              . 'AND exemplaires.shelf_key_update_date < \'%s\' ))',
+              Class_CosmoVar::getValueOf('shelf_key_update_date') ?? '0000-00-00 00:00:00',
+              $this->_current_date_time);
+
+    $select_columns =
+      sprintf('select exemplaires.id, exemplaires.id_notice, '
+              . 'exemplaires.cote, notices.clef_chapeau, notices.tome_alpha, '
+              . 'notices.clef_alpha '
+              . 'from exemplaires inner join notices on exemplaires.id_notice = notices.id_notice '
+              . 'where %s limit %d',
+              $where_clause,
+              $this->_page_size);
+
+    $count_result =
+      $this->_sql->fetchAllByColumn(sprintf('select count(exemplaires.id) from exemplaires where %s', $where_clause));
+    $count = reset($count_result);
+
+    $this->echoError($this->_('Nombre de clés à indexer : %d', $count) . "\n");
+
+    if ( 0 === $count)
+      return $this;
+
+    $page_count = 0;
+    $total_pages = (int) ceil(($count / $this->_page_size));
+
+    while ($page = $this->_sql->fetchAll($select_columns)) {
+      $page_count++;
+
+      if ( $page_count > $total_pages ) {
+        $this->echoEndTitle($this->_('%s stopée.',
+                                     $title));
+        return $this;
+      }
+
+      $this->_runPage($page, $page_count, $total_pages);
+    }
+
+    $this->echoOK();
+    $this->echoEndTitle($this->_('%s traitée en %ss',
+                                 $title,
+                                 $this->_chrono->elapsedOnFile()));
+    return $this;
+  }
+
+
+  protected function _runPage(array $page, int $count, int $total) : void {
+    $this->_chrono->startOnRecords();
+    $this->echoError($this->_('page de %d exemplaires : %d/%d',
+                              count($page),
+                              $count,
+                              $total));
+
+    $update_items =
+      sprintf('update exemplaires '
+              . 'set shelf_key = (CASE %s ELSE shelf_key END), shelf_key_update_date = \'%s\' '
+              . 'WHERE id in (%s)',
+              $this->_whenThenForPage($page),
+              $this->_current_date_time,
+              $this->_idsForWhere($page));
+
+    $this->_sql->execute($update_items);
+
+    $this->echoError($this->_(' en %ss', $this->_chrono->elapsedOnRecords()) . "\n");
+  }
+
+
+  protected function _whenThenForPage(array $page) : string {
+    return implode(' ', array_map(fn($row) => $this->_whenThenForRow($row), $page));
+  }
+
+
+  protected function _whenThenForRow(array $row) : string {
+    return sprintf('WHEN (id = "%s") THEN "%s"',
+                   $row['id'],
+                   $this->_shelf_key_generator->generateForArray($row));
+  }
+
+
+  protected function _idsForWhere(array $page) {
+    return implode(',', array_map(fn($row) => $row['id'], $page));
+  }
+}
\ No newline at end of file
diff --git a/library/Class/Profil/ItemsSettings.php b/library/Class/Profil/ItemsSettings.php
index c6c4d7ab43b37b3ea4e27c40f0d103ce3faf4246..7f644e03faa7bb13eeb45e1e077fd3e4de1e3c0f 100644
--- a/library/Class/Profil/ItemsSettings.php
+++ b/library/Class/Profil/ItemsSettings.php
@@ -48,6 +48,11 @@ class Class_Profil_ItemsSettings  {
   }
 
 
+  public function isItemsShelfEnabled() : bool {
+    return (bool) ($this->_settings['enable_items_shelf'] ?? 0);
+  }
+
+
   public function setSettings($settings) {
     $this->_settings = array_merge($this->_settings, $settings);
 
diff --git a/library/Class/Template/Update.php b/library/Class/Template/Update.php
index 71e3056266cf5590b6dd2fdd545c065f840540f6..7f0ee0de366188502ae7aea060150ce1c18932a6 100644
--- a/library/Class/Template/Update.php
+++ b/library/Class/Template/Update.php
@@ -43,11 +43,4 @@ class Class_Template_Update {
 
     $this->echoEndTitle($this->_('Fin de la mise à jour des thèmes du magasin'));
   }
-
-
-  public function runWithEcho(bool $echo) : self {
-    $this->setEcho($echo);
-    $this->run();
-    return $this;
-  }
 }
diff --git a/library/Trait/EchoError.php b/library/Trait/EchoError.php
index 7039e34e26ef0745212feb17e371a58ec9935134..cadcd1ee6daaad51c0c1ac5f4fd8139240d954e5 100644
--- a/library/Trait/EchoError.php
+++ b/library/Trait/EchoError.php
@@ -21,14 +21,24 @@
 
 
 trait Trait_EchoError {
-  protected static $_echo;
-
-  public function echoError($error) {
-    return call_user_func(function($args) {
-      if(!self::$_echo)
-        echo $args;
-      return $args;
-    },$error);
+
+  protected $_echo_function;
+
+
+  public function setEchoFunction(Closure $echo_function) : self {
+    $this->_echo_function = $echo_function;
+    return $this;
+  }
+
+
+  public function echoError($message) : self {
+    if ($this->_echo_function) {
+      call_user_func($this->_echo_function, $message);
+      return $this;
+    }
+
+    echo $message;
+    return $this;
   }
 
 
@@ -47,7 +57,9 @@ trait Trait_EchoError {
   }
 
 
-  public static function setEcho($echo) {
-    self::$_echo = $echo;
+  public function runWithoutEcho() : self {
+    $this->setEchoFunction(fn() => null);
+    call_user_func_array([$this, 'run'], func_get_args());
+    return $this;
   }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Notice/Unimarc.php b/library/ZendAfi/View/Helper/Notice/Unimarc.php
index 2a9d0b56041cce486f7b8cd0c1bad118904c2859..87f9a75d3113550c5526f55394099de8f7bec263 100644
--- a/library/ZendAfi/View/Helper/Notice/Unimarc.php
+++ b/library/ZendAfi/View/Helper/Notice/Unimarc.php
@@ -136,7 +136,8 @@ class ZendAfi_View_Helper_Notice_Unimarc extends Zend_View_Helper_HtmlElement {
                 'getIdOrigine' => $this->_('Id origine'),
                 'getIdDataProfile' => $this->_('Profil de données'),
                 'getDateNouveaute' => $this->_('Date nouveauté'),
-                'getIdIntBib' => $this->_('Intégration programmée')];
+                'getIdIntBib' => $this->_('Intégration programmée'),
+                'getShelfKey' => $this->_('Clé étagère')];
 
     $html = '';
     foreach($notice->getExemplaires() as $item)
diff --git a/library/ZendAfi/View/Helper/Template/BadgeGroup.php b/library/ZendAfi/View/Helper/Template/BadgeGroup.php
index 501ea2ce9ee460d2ca8349fa74dc5903823c9493..c5912a798bb8870f0641ee95c23a390efacd4e7f 100644
--- a/library/ZendAfi/View/Helper/Template/BadgeGroup.php
+++ b/library/ZendAfi/View/Helper/Template/BadgeGroup.php
@@ -38,6 +38,9 @@ class ZendAfi_View_Helper_Template_BadgeGroup extends ZendAfi_View_Helper_BaseHe
                   if ($url = $badge->getUrl())
                     $attribs['href'] = $url;
 
+                  if ($on_click = $badge->getOnClick())
+                    $attribs['onclick'] = $on_click;
+
                   $img = ($img = $badge->getImage())
                     ? $img
                     : '';
diff --git a/library/ZendAfi/View/Helper/Template/ItemShelf.php b/library/ZendAfi/View/Helper/Template/ItemShelf.php
new file mode 100644
index 0000000000000000000000000000000000000000..df6b485f56b8b0cf927aa3ab586e5b3c94667371
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Template/ItemShelf.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class ZendAfi_View_Helper_Template_ItemShelf
+  extends ZendAfi_View_Helper_BaseHelper {
+
+  public function itemShelf(Class_Exemplaire_Shelf $shelf) : string {
+    return
+      $this->_tag('h2', $this->_('Étagère de la bibliothèque %s',
+                                 $shelf->getBibLibelle()))
+      .
+      $shelf->renderOn(fn($before, $item, $after) => $this->_renderShelf($before, $item, $after));
+  }
+
+
+  protected function _renderShelf(array $before_items,
+                                  Class_Exemplaire $selected_item,
+                                  array $after_items) : string {
+
+    return $this->view
+      ->renderMultipleCarousel($this->_itemsToWrappedRecords([...$before_items,
+                                                              $selected_item,
+                                                              ...$after_items]),
+
+                               fn($record_wrapper) => $this->_renderRecord($record_wrapper,
+                                                                           $selected_item),
+                               (new Intonation_Library_Widget_Carousel_Settings)
+                               ->setColumns(5)
+                               ->setActivePage((int)(count($before_items) / 5)));
+  }
+
+
+  protected function _itemsToWrappedRecords(array $items) : Storm_Model_Collection {
+    $records = new Storm_Model_Collection;
+    foreach($items as $item)
+      $records->add((new Intonation_Library_View_Wrapper_Record($item->getNotice()))
+                    ->setView($this->view));
+
+    return $records;
+  }
+
+
+  protected function _renderRecord(Intonation_Library_View_Wrapper_Record $record_wrapper, Class_Exemplaire $selected_item) : string {
+    return $record_wrapper->getId() === $selected_item->getIdNotice()
+      ? $this->_renderRecordEmphasized($record_wrapper)
+      : $this->view->renderingOnlyImage($record_wrapper);
+  }
+
+
+  protected function _renderRecordEmphasized(Intonation_Library_View_Wrapper_Record $record_wrapper) : string {
+    return (new class() extends ZendAfi_View_Helper_Template_RenderingOnlyImage {
+              protected function _cardClass() : string {
+                return parent::_cardClass() . ' shelf_current_item';
+              }
+           })
+      ->setView($this->view)
+      ->renderingOnlyImage($record_wrapper);
+  }
+}
diff --git a/library/ZendAfi/View/Helper/Template/Jumbotron.php b/library/ZendAfi/View/Helper/Template/Jumbotron.php
index 63dca1ddf5eea9820ab5fa3817b900dfe0869c14..058ceba3b181f42c138856a3801691f6e180788a 100644
--- a/library/ZendAfi/View/Helper/Template/Jumbotron.php
+++ b/library/ZendAfi/View/Helper/Template/Jumbotron.php
@@ -235,9 +235,7 @@ class ZendAfi_View_Helper_Template_Jumbotron extends ZendAfi_View_Helper_BaseHel
 
 
   protected function _renderLoadingIcon() {
-    return $this->_div(['class' => 'col-12 text-center'],
-                       $this->_div(['class' => 'loading_icon'],
-                                   $this->view->screenReaderOnly($this->_('Merci de patientier...'))));
+    return $this->view->loadingIcon();
   }
 
 
diff --git a/library/ZendAfi/View/Helper/Template/LayoutCarousel.php b/library/ZendAfi/View/Helper/Template/LayoutCarousel.php
index bf535f79c0e79a9da0ba18c81416c60df262fa74..041bd202155b2b601070ef32a5c736e487d69eb0 100644
--- a/library/ZendAfi/View/Helper/Template/LayoutCarousel.php
+++ b/library/ZendAfi/View/Helper/Template/LayoutCarousel.php
@@ -22,8 +22,10 @@
 
 class ZendAfi_View_Helper_Template_LayoutCarousel extends Intonation_View_Abstract_Carousel {
 
-  public function layoutCarousel($collection, $callback = null) {
-    return $this->_renderCarousel($collection, $callback);
+  public function layoutCarousel($collection,
+                                 $callback = null,
+                                 Intonation_Library_Widget_Carousel_Settings $settings) {
+    return $this->_renderCarousel($collection, $callback, $settings);
   }
 
 
@@ -40,4 +42,4 @@ class ZendAfi_View_Helper_Template_LayoutCarousel extends Intonation_View_Abstra
   protected function _shouldShowControls($collection) {
     return 1 < $this->_numberOfPages($collection);
   }
-}
\ No newline at end of file
+}
diff --git a/library/ZendAfi/View/Helper/Template/LoadingIcon.php b/library/ZendAfi/View/Helper/Template/LoadingIcon.php
new file mode 100644
index 0000000000000000000000000000000000000000..e9be8d7f14605624f50c9bee57ecfd35e913519c
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Template/LoadingIcon.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class ZendAfi_View_Helper_Template_LoadingIcon extends ZendAfi_View_Helper_BaseHelper {
+  public function loadingIcon() : string {
+    return $this->_div(['class' => 'col-12 text-center'],
+                       $this->_div(['class' => 'loading_icon'],
+                                   $this->view->screenReaderOnly($this->_('Merci de patientier...'))));
+  }
+}
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Template/RenderingOnlyImage.php b/library/ZendAfi/View/Helper/Template/RenderingOnlyImage.php
index 5ef02f205fba4a1f0f95d9f332ca9f74d96f3661..6087128798bb1dc2cfb497e4cf038d2c18fffb56 100644
--- a/library/ZendAfi/View/Helper/Template/RenderingOnlyImage.php
+++ b/library/ZendAfi/View/Helper/Template/RenderingOnlyImage.php
@@ -27,7 +27,12 @@ class ZendAfi_View_Helper_Template_RenderingOnlyImage
     return $this->_tag('div',
                        $element->getAnchor()
                        . $this->_cardWithPicture($element),
-                       ['class' => 'card_with_overlay']);
+                       ['class' => $this->_cardClass()]);
+  }
+
+
+  protected function _cardClass() : string {
+    return 'card_with_overlay';
   }
 
 
diff --git a/library/storm b/library/storm
index c153d5d9c58e5823df94410ee1c6ae321d2bf51b..55dd4c19b54eead3f8a06871cbca07d582973427 160000
--- a/library/storm
+++ b/library/storm
@@ -1 +1 @@
-Subproject commit c153d5d9c58e5823df94410ee1c6ae321d2bf51b
+Subproject commit 55dd4c19b54eead3f8a06871cbca07d582973427
diff --git a/library/templates/Chili/View/Abonne.php b/library/templates/Chili/View/Abonne.php
index 1a78a3b1379b2782bfab068abb119ab956ae60c0..4eca0ce96bed41d12a2a65bbc446c96c88f9cae6 100644
--- a/library/templates/Chili/View/Abonne.php
+++ b/library/templates/Chili/View/Abonne.php
@@ -51,9 +51,11 @@ class Chili_View_Abonne extends ZendAfi_View_Helper_BaseHelper {
 
 
   protected function _renderCarouselForSmallScreen($sections) {
-    $content = $this->view->renderMultipleCarousel($sections,
-                                                   [$this->view, 'renderingVertical'],
-                                                   3);
+    $content =
+      $this->view->renderMultipleCarousel($sections,
+                                          [$this->view, 'renderingVertical'],
+                                          ((new Intonation_Library_Widget_Carousel_Settings)
+                                           ->setColumns(3)));
 
     return $this->_div(['class' => 'big_buttons_sm'],
                        $content);
diff --git a/library/templates/Chili/View/AbstractHighlightCarousel.php b/library/templates/Chili/View/AbstractHighlightCarousel.php
index 0455dacbd518ff50339d283d7210084854d643fe..fb3ca13f13b1ccd18fe121b068ffc97db462e716 100644
--- a/library/templates/Chili/View/AbstractHighlightCarousel.php
+++ b/library/templates/Chili/View/AbstractHighlightCarousel.php
@@ -20,66 +20,77 @@
  */
 
 
-class Chili_View_AbstractHighlightCarousel extends Intonation_View_RenderMultipleCarousel {
+class Chili_View_AbstractHighlightCarousel
+  extends Intonation_View_RenderMultipleCarousel {
 
-  protected
-    $_columns,
-    $_highlight_class,
+  protected $_highlight_class,
     $_highlight_column_class,
     $_columns_class;
 
 
-  protected function _renderCarousel($collection, $callback) {
+  protected function _renderCarousel(Storm_Collection $collection,
+                                     ?Callable $callback,
+                                     Intonation_Library_Widget_Carousel_Settings $settings) {
+    if ($collection->isEmpty())
+      return '';
+
+    $this->_settings = $settings;
+
     $html = [$this->_div(['class' => 'highlight_carousel_xl'],
-                         parent::_renderCarousel($collection, $callback)),
+                         parent::_renderCarousel($collection, $callback, $settings)),
 
              $this->_div(['class' => 'highlight_carousel_md'],
-                         $this->view->renderMultipleCarousel($collection, $callback, 3)),
+                         $this->view->renderMultipleCarousel($collection,
+                                                             $callback,
+                                                             $settings->setColumns(3))),
 
              $this->_div(['class' => 'highlight_carousel_sm'],
-                         $this->view->layoutCarousel($collection, $callback))];
+                         $this->view->layoutCarousel($collection,
+                                                     $callback,
+                                                     $settings->setColumns(1)))];
 
     return $this->view->grid($html);
   }
 
 
-  protected function _carouselInner($collection, $id, $callback) {
+  protected function _carouselInner(Storm_Collection $collection,
+                                    string $id,
+                                    Callable $callback) : string {
     $cards = array_filter($collection->injectInto([], function($html, $element) use ($callback)
     {
       $count = count($html);
-      $html [] = ($count % $this->_columns != 0)
+      $html [] = ($count % $this->_settings->getColumns() != 0)
       ? $callback($element)
       : $this->view->highlightCardify($element);
       return $html;
     }));
 
-    return Intonation_View_Abstract_Carousel::_carouselInner($this->_gridify($cards), $id, function ($element)
-                                  {
-                                    return $element;
-                                  });
+    return Intonation_View_Abstract_Carousel::_carouselInner($this->_gridify($cards),
+                                                             $id,
+                                                             fn($element) => $element);
   }
 
 
   protected function _gridify($elements) {
-    $number_of_rows = ceil(count($elements) / $this->_columns);
+    $number_of_rows = ceil(count($elements) / $this->_settings->getColumns());
 
     $rows = [];
 
     for ($i = 0; $i < $number_of_rows; $i++) {
       $items = array_slice($elements,
-                           $i * $this->_columns,
-                           $this->_columns);
+                           $i * $this->_settings->getColumns(),
+                           $this->_settings->getColumns());
 
       $highlight = array_shift($items);
 
       $collection_html = $this->_tag('div',
                                      implode($items),
-                                     ['class' => $this->_getWrapperClass($items, $this->_columns - 1)]);
+                                     ['class' => $this->_getWrapperClass($items, $this->_settings->getColumns() - 1)]);
 
       $rows [$i] = $this->view->grid(implode([$this->view->div(['class' => $this->_highlight_column_class],
-                                                        $highlight),
-                                       $this->view->div(['class' => $this->_columns_class],
-                                                        $collection_html)]));
+                                                               $highlight),
+                                              $this->view->div(['class' => $this->_columns_class],
+                                                               $collection_html)]));
     }
 
     return new Storm_Collection($rows);
diff --git a/library/templates/Chili/View/RenderLeftHighlightCarousel.php b/library/templates/Chili/View/RenderLeftHighlightCarousel.php
index b44ab67f533470877288aaa1b3e5a6c156ded2c4..bfeeff07eec9482b9b626b33fd229b87089d05a7 100644
--- a/library/templates/Chili/View/RenderLeftHighlightCarousel.php
+++ b/library/templates/Chili/View/RenderLeftHighlightCarousel.php
@@ -20,16 +20,18 @@
  */
 
 
-class Chili_View_RenderLeftHighlightCarousel extends Chili_View_AbstractHighlightCarousel {
+class Chili_View_RenderLeftHighlightCarousel
+  extends Chili_View_AbstractHighlightCarousel {
 
-  protected
-    $_columns = 4,
-    $_highlight_class = 'left_highlight_carousel',
+  protected $_highlight_class = 'left_highlight_carousel',
     $_highlight_column_class = 'left_highlight_column',
     $_columns_class = 'left_carousel_columns';
 
 
   public function renderLeftHighlightCarousel($elements, $content_callback) {
-    return $this->_renderCarousel($elements, $content_callback);
+    return $this->_renderCarousel($elements,
+                                  $content_callback,
+                                  ((new Intonation_Library_Widget_Carousel_Settings)
+                                   ->setColumns(4)));
   }
 }
diff --git a/library/templates/Chili/View/RenderMultipleCarousel.php b/library/templates/Chili/View/RenderMultipleCarousel.php
index 91b03eca6ca5ce649f6822c9d33d03e02a22fb31..67a65582b768681bd1febf3803860be2988b2328 100644
--- a/library/templates/Chili/View/RenderMultipleCarousel.php
+++ b/library/templates/Chili/View/RenderMultipleCarousel.php
@@ -20,22 +20,37 @@
  */
 
 
-class Chili_View_RenderMultipleCarousel extends Intonation_View_RenderMultipleCarousel {
+class Chili_View_RenderMultipleCarousel
+  extends Intonation_View_RenderMultipleCarousel {
+
+  protected function _renderCarousel($collection,
+                                     $callback,
+                                     Intonation_Library_Widget_Carousel_Settings $settings) {
+
+    if ($collection->isEmpty())
+      return '';
+
+    $this->_settings = $settings;
 
-  protected function _renderCarousel($collection, $callback) {
     $md_helper = (new Intonation_View_RenderMultipleCarousel)
       ->setView($this->view);
 
     $html = [
-             $this->_div(['class' => 'col-12 d-none d-lg-block'], parent::_renderCarousel($collection, $callback)),
+             $this->_div(['class' => 'col-12 d-none d-lg-block'],
+                         parent::_renderCarousel($collection,
+                                                 $callback,
+                                                 $settings->setColumns(5))),
 
              $this->_div(['class' => 'col-12 d-none d-md-block d-lg-none'],
                          $md_helper->renderMultipleCarousel($collection,
                                                             $callback,
-                                                            3)),
+                                                            ($settings
+                                                             ->setColumns(3)))),
 
-             $this->_div(['class' => 'col-12 d-block d-md-none'], $this->view->layoutCarousel($collection, $callback)),
-    ];
+             $this->_div(['class' => 'col-12 d-block d-md-none'],
+                         $this->view->layoutCarousel($collection,
+                                                     $callback,
+                                                     $settings->setColumns(1)))];
 
     return $this->view->grid($html, ['class' => 'responsive_multiple_carousel']);
   }
diff --git a/library/templates/Chili/View/RenderTopHighlightCarousel.php b/library/templates/Chili/View/RenderTopHighlightCarousel.php
index 0c2d6a87dc1ec94fdb5b85a4c75a8d3cc0e965e5..729291c8276f5f208d095b8cd294fa91188016b7 100644
--- a/library/templates/Chili/View/RenderTopHighlightCarousel.php
+++ b/library/templates/Chili/View/RenderTopHighlightCarousel.php
@@ -20,16 +20,17 @@
  */
 
 
-class Chili_View_RenderTopHighlightCarousel extends Chili_View_AbstractHighlightCarousel {
+class Chili_View_RenderTopHighlightCarousel
+  extends Chili_View_AbstractHighlightCarousel {
 
-  protected
-    $_columns = 6,
-    $_highlight_class = 'top_highlight_carousel',
+  protected $_highlight_class = 'top_highlight_carousel',
     $_highlight_column_class = 'top_highlight_column',
     $_columns_class = 'top_carousel_columns';
 
 
   public function renderTopHighlightCarousel($elements, $content_callback) {
-    return $this->_renderCarousel($elements, $content_callback);
+    return $this->_renderCarousel($elements,
+                                  $content_callback,
+                                  (new Intonation_Library_Widget_Carousel_Settings)->setColumns(6));
   }
-}
\ No newline at end of file
+}
diff --git a/library/templates/Chili/View/RenderWall.php b/library/templates/Chili/View/RenderWall.php
index dcd457d567a8ffff987490c099b8b5313e09b0cf..0887f94d2e3d05b0c1c2dd0a179f0ad9ae247c79 100644
--- a/library/templates/Chili/View/RenderWall.php
+++ b/library/templates/Chili/View/RenderWall.php
@@ -36,11 +36,14 @@ class Chili_View_RenderWall extends Intonation_View_RenderWall {
 
 
   protected function _renderHtml($html) {
-    $html = [$this->_div(['class' => 'wall_grid_lg'],
-                         parent::_renderHtml($html)),
-
-             $this->_div(['class' => 'wall_grid_md'],
-                         $this->view->renderMultipleCarousel($this->_collection, $this->_callback, 3))];
+    $html =
+      [$this->_div(['class' => 'wall_grid_lg'],
+                   parent::_renderHtml($html)),
+
+       $this->_div(['class' => 'wall_grid_md'],
+                   $this->view->renderMultipleCarousel($this->_collection,
+                                                       $this->_callback,
+                                                       (new Intonation_Library_Widget_Carousel_Settings)->setColumns(3)))];
 
     return $this->view->grid($html);
   }
diff --git a/library/templates/Herisson/Assets/css/herisson.css b/library/templates/Herisson/Assets/css/herisson.css
index c9ef5c34e129ecb67ecf9ec422b5e1d60a11fb7d..6e40b5d6d7a2aeae7efd8ca3f072c14e6bf97edc 100644
--- a/library/templates/Herisson/Assets/css/herisson.css
+++ b/library/templates/Herisson/Assets/css/herisson.css
@@ -1106,6 +1106,11 @@ h2.jumbotron_section_title * {
     min-width: 100px;
 }
 
+a.badge_tag.badge-secondary {
+    background-color: var(--background-very-dark);
+    border-color: var(--background-very-dark);
+}
+
 
 /*fiche bib*/
 .wrapper_library_openings .default_opening_hours h3 {
diff --git a/library/templates/Intonation/Assets/css/intonation.css b/library/templates/Intonation/Assets/css/intonation.css
index 7e1d8b61067304b146f94588938d390246305d51..dceef44bcf29dc24337a6527b9af6a320103e8c5 100644
--- a/library/templates/Intonation/Assets/css/intonation.css
+++ b/library/templates/Intonation/Assets/css/intonation.css
@@ -1149,3 +1149,13 @@ button.view_more_record_actions,
 .navbar_toggler_text {
     display: none;
 }
+
+#items_shelf .card_with_overlay {
+    height: 80%;
+    margin: auto;
+}
+
+#items_shelf .card_with_overlay.shelf_current_item {
+    height: 90%;
+    box-shadow: 0px 0px 20px 1px --front-shadow;
+}
diff --git a/library/templates/Intonation/Assets/js/item_shelf/item_shelf.js b/library/templates/Intonation/Assets/js/item_shelf/item_shelf.js
new file mode 100644
index 0000000000000000000000000000000000000000..f28afccaa755ebaf0f7efbe9424398282047c796
--- /dev/null
+++ b/library/templates/Intonation/Assets/js/item_shelf/item_shelf.js
@@ -0,0 +1,10 @@
+(function ( $ ) {
+  $.fn.item_shelf = function(event) {
+    event.preventDefault();
+    var link = $(this).closest('a');
+    var url = link.attr("href");
+    var items_shelf = $("#items_shelf");
+    items_shelf.addClass('loading_data');
+    items_shelf.load(url + "/render/ajax", () => items_shelf.removeClass('loading_data'));
+  };
+} (jQuery));
diff --git a/library/templates/Intonation/Assets/js/item_shelf/item_shelf_tests.html b/library/templates/Intonation/Assets/js/item_shelf/item_shelf_tests.html
new file mode 100644
index 0000000000000000000000000000000000000000..baca0f8d9bf89a891c20e72a69b84c5372ead19d
--- /dev/null
+++ b/library/templates/Intonation/Assets/js/item_shelf/item_shelf_tests.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+    /**
+    * Copyright (c) 2022, Agence Française Informatique (AFI). All rights reserved.
+    *
+    * BOKEH is free software; you can redistribute it and/or modify
+    * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+    * the Free Software Foundation.
+    *
+    * There are special exceptions to the terms and conditions of the AGPL as it
+    * is applied to this software (see README file).
+    *
+    * BOKEH is distributed in the hope that it will be useful,
+    * but WITHOUT ANY WARRANTY; without even the implied warranty of
+    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+    *
+    * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+    * along with BOKEH; if not, write to the Free Software
+    * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA 
+    */
+  -->
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Item Shelf tests</title>
+    <link rel="stylesheet" href="../../../../../../public/qunit-git.css">
+
+    <script src="../../../../../../public/qunit-1.13.0.js"></script>
+    <script src="../../../../../../public/admin/js/jquery-3.6.0.min.js"></script>
+    
+    <script src="item_shelf.js"></script>
+    <script src="item_shelf_tests.js"></script>
+    
+  </head>
+  <body>
+    <div id="qunit"></div>
+    <div >
+      <a title="Parcourir l'étagère de la bibliothèque Astrolabe" class="badge_tag browse_shelf text-left badge badge-secondary" href="/recherche/shelf/item_id/999999" onclick="$(this).item_shelf(event);"><i class="fas fa-glasses" aria-hidden="true"></i><span class="badge_text align-middle d-inline-block text-left"><span class="sr-only"> de la bibliothèque Astrolabe</span>Parcourir l'étagère</span></a>
+      <div id="items_shelf" class="items_shelf col-12"></div>
+    </div>
+</head>
+</html>
diff --git a/library/templates/Intonation/Assets/js/item_shelf/item_shelf_tests.js b/library/templates/Intonation/Assets/js/item_shelf/item_shelf_tests.js
new file mode 100644
index 0000000000000000000000000000000000000000..9d447dd7cdae45e07ddab88cb057a3c3d3e36a2e
--- /dev/null
+++ b/library/templates/Intonation/Assets/js/item_shelf/item_shelf_tests.js
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+QUnit.module('item_shelf');
+
+var expected_url;
+var delayed_callback;
+
+
+moduleStart( () => {
+  $.fn.load = (url, callback) => {
+    expected_url = url;
+    delayed_callback = callback;
+  };
+});
+
+
+test('click on anchor should add class loading_data to div id items_shelf', () => {
+  $("a").click();
+  deepEqual($('#items_shelf.loading_data').length, 1);
+});
+
+
+test('click on anchor should call jquery load with url /recherche/shelf/item_id/999999/render/ajax', () => {
+  $("a").click();
+  deepEqual(expected_url, '/recherche/shelf/item_id/999999/render/ajax');
+});
+
+
+test('click on anchor after load should remove class loading_data from div id items_shelf', () => {
+  $("a").click();
+  delayed_callback();
+  deepEqual($('#items_shelf:not(.loading_data)').length, 1);
+});
diff --git a/library/templates/Intonation/Library/Badge.php b/library/templates/Intonation/Library/Badge.php
index 737fa92fc3d31c46ffa60b2fa142a516b4245f1d..9af935cd199002a767ff30fc9cea2ee4f95b207b 100644
--- a/library/templates/Intonation/Library/Badge.php
+++ b/library/templates/Intonation/Library/Badge.php
@@ -28,7 +28,8 @@ class Intonation_Library_Badge {
     $_image,
     $_class = 'secondary',
     $_tag = 'span',
-    $_url;
+    $_url,
+    $_on_click;
 
 
   public function setText($text) {
@@ -86,13 +87,24 @@ class Intonation_Library_Badge {
   }
 
 
-  public function setUrl($url) {
+  public function setUrl(string $url) : self {
     $this->_url = $url;
     return $this;
   }
 
 
-  public function getUrl() {
+  public function getUrl() : ?string {
     return $this->_url;
   }
+
+
+  public function setOnClick(string $code) : self {
+    $this->_on_click = $code;
+    return $this;
+  }
+
+
+  public function getOnClick() : string {
+    return $this->_on_click ?? '';
+  }
 }
diff --git a/library/templates/Intonation/Library/FormCustomizer/RecordItems.php b/library/templates/Intonation/Library/FormCustomizer/RecordItems.php
index 66316bab09714fa2c9ae2efaf0205922ee69638b..932197a1a0bd364f0afd1e92662e816f0b03c670 100644
--- a/library/templates/Intonation/Library/FormCustomizer/RecordItems.php
+++ b/library/templates/Intonation/Library/FormCustomizer/RecordItems.php
@@ -37,11 +37,15 @@ class Intonation_Library_FormCustomizer_RecordItems extends Intonation_Library_F
     $this->_form->addElement('checkbox',
                              'all_items_map',
                              ['label' => $this->_('Afficher la carte des exemplaires')])
+                ->addElement('checkbox',
+                             'enable_items_shelf',
+                             ['label' => $this->_('Activer la navigation par étagère')])
                 ->addElement('number',
                              'pagination_threshold',
                              ['label' => $this->_('Pagine les exemplaires à partir de'),
                              'value' => 200])
                 ->addToDisplayGroup(['all_items_map',
+                                     'enable_items_shelf',
                                      'pagination_threshold'],
                                     'items_fieldset');
     return $this->_form;
diff --git a/library/templates/Intonation/Library/Settings.php b/library/templates/Intonation/Library/Settings.php
index 161225ec10ca8682e1e0b080607c699da604cb18..35ddda527e6474dd35420e7614faba0c9a41f493 100644
--- a/library/templates/Intonation/Library/Settings.php
+++ b/library/templates/Intonation/Library/Settings.php
@@ -344,6 +344,8 @@ class Intonation_Library_Settings extends Intonation_System_Abstract {
                                                   'span class danger' => 'badge-danger text-light',
                                                   'span class success' => 'badge-success text-light',
                                                   'span class warning' => 'badge-warning text-dark',
+                                                  'a class browse_shelf' => 'badge-secondary',
+                                                  'div class items_shelf' => 'col-12',
                           ],
 
                           'icons_map_doc_types' => [],
diff --git a/library/templates/Intonation/Library/View/Wrapper/Item.php b/library/templates/Intonation/Library/View/Wrapper/Item.php
index 888f531ac7ee632fcc4bf6c3c258112c3489c4cf..2c6bcbe3b6904c378a944d734853dfe237b97cce 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Item.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Item.php
@@ -113,14 +113,13 @@ class Intonation_Library_View_Wrapper_Item extends Intonation_Library_View_Wrapp
 
 
   public function getBadges() {
-    $badges [] = $this->_getSectionBadge();
-    $badges [] = $this->_getEmplacementBadge();
-    $badges [] = $this->_getCoteBadge();
-    $badges [] = $this->_getDateRetourBadge();
-    $badges [] = $this->_getReservationsBadge();
-
-    foreach( $this->getDatasItemsBadges() as $item_badge)
-      $badges[] = $item_badge;
+    $badges = [$this->_getSectionBadge(),
+               $this->_getEmplacementBadge(),
+               $this->_getCoteBadge(),
+               $this->_getBrowseShelfBadge(),
+               $this->_getDateRetourBadge(),
+               $this->_getReservationsBadge(),
+               ...$this->getDatasItemsBadges()];
 
     return $this->_view->badgeGroup(array_filter($badges), $this);
   }
@@ -152,11 +151,34 @@ class Intonation_Library_View_Wrapper_Item extends Intonation_Library_View_Wrapp
 
     $action
       ->setImage($this->getSecondaryIco())
-      ->setText($action->getAttrib('text-hold', $action->getText()));
+      ->setText($action->getAttrib('text-hold',
+                                   $action->getText()));
+
     return [$action];
   }
 
 
+  protected function _getBrowseShelfBadge() : ?Intonation_Library_Badge {
+    if ( ! Class_Profil_ItemsSettings::current()->isItemsShelfEnabled())
+      return null;
+
+    if ( ! $this->_model->getShelfKey())
+      return null;
+
+    return (new Intonation_Library_Badge)
+      ->setTag('a')
+      ->setUrl(Class_Url::relative(['controller' => 'recherche',
+                                    'action' => 'shelf',
+                                    'item_id' => $this->_model->getId()]))
+      ->setClass('browse_shelf')
+      ->setText($this->_('Parcourir l\'étagère'))
+      ->setImage($this->_view->renderIcon('class fas fa-glasses'))
+      ->setTitle($this->_('Parcourir l\'étagère de la bibliothèque %s',
+                          $this->_model->getBibLibelle()))
+      ->setOnClick('$(this).item_shelf(event);');
+  }
+
+
   public function getEmbedMedia() {
     return '';
   }
@@ -190,7 +212,6 @@ class Intonation_Library_View_Wrapper_Item extends Intonation_Library_View_Wrapp
       return null;
 
     return (new Intonation_Library_Badge)
-      ->setTag('span')
       ->setClass('badge-secondary badge-emplacement')
       ->setImage($this->getIco('place', 'library'))
       ->setText(Class_CodifEmplacement::getLabel($this->_model->getEmplacement()))
@@ -216,7 +237,6 @@ class Intonation_Library_View_Wrapper_Item extends Intonation_Library_View_Wrapp
       return null;
 
     return (new Intonation_Library_Badge)
-      ->setTag('span')
       ->setClass('badge-warning')
       ->setImage($this->getIco('return-date', 'library'))
       ->setText($this->_model->getDateRetour())
@@ -230,7 +250,6 @@ class Intonation_Library_View_Wrapper_Item extends Intonation_Library_View_Wrapp
       return null;
 
     return (new Intonation_Library_Badge)
-      ->setTag('span')
       ->setClass('item_hold_rank badge-warning')
       ->setImage($this->getIco('hold', 'library'))
       ->setText($this->_plural($this->_getCurrentHoldsCount(),
diff --git a/library/templates/Intonation/Library/View/Wrapper/Library/RichContent/Team.php b/library/templates/Intonation/Library/View/Wrapper/Library/RichContent/Team.php
index 792194c36ea392dd6ef459a5c7ab3db11bf8f58a..93c3547a914792229f5e328875283234541e4795 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Library/RichContent/Team.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Library/RichContent/Team.php
@@ -71,7 +71,9 @@ class Intonation_Library_View_Wrapper_Library_RichContent_Team extends Intonatio
       return $this->_view->renderingVertical($wrapped);
     };
 
-    return $this->_view->renderMultipleCarousel(new Storm_Collection($pros), $callback);
+    return $this->_view->renderMultipleCarousel(new Storm_Collection($pros),
+                                                $callback,
+                                                (new Intonation_Library_Widget_Carousel_Settings)->setColumns(5));
   }
 
 
diff --git a/library/templates/Intonation/Library/View/Wrapper/Record/RichContent/Related.php b/library/templates/Intonation/Library/View/Wrapper/Record/RichContent/Related.php
index 19f045bdcbe76180a4a49070f1a22cfdeac21f83..859f7bb42103a4ad6bc19c7a31b169a90544b78a 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Record/RichContent/Related.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Record/RichContent/Related.php
@@ -113,13 +113,15 @@ class Intonation_Library_View_Wrapper_Record_RichContent_RelatedForRecord
       $html [] = $view->div(['class' => 'col-12 mt-3'],
                             $view->tag('h2', $this->_('Les documents de la même série'))
                             . $view->renderMultipleCarousel(new Storm_Collection($same_series),
-                                                            $this->_callback));
+                                                            $this->_callback,
+                                                            (new Intonation_Library_Widget_Carousel_Settings)->setColumns(5)));
 
     if ($like = $model->getNoticesSimilaires())
       $html [] = $view->div(['class' => 'col-12 mt-3'],
                             $view->tag('h2', $this->_('Les similaires'))
                             . $view->renderMultipleCarousel(new Storm_Collection($like),
-                                                            $this->_callback));
+                                                            $this->_callback,
+                                                            (new Intonation_Library_Widget_Carousel_Settings)->setColumns(5)));
 
     return implode($html);
   }
@@ -147,7 +149,8 @@ class Intonation_Library_View_Wrapper_Record_RichContent_RelatedForWork
     if ($editions = $model->getNoticesSameWork())
       $html [] = $view->div(['class' => 'col-12 mt-3'],
                             $view->renderMultipleCarousel(new Storm_Collection($editions),
-                                                          $this->_callback));
+                                                          $this->_callback,
+                                                          (new Intonation_Library_Widget_Carousel_Settings)->setColumns(5)));
 
     return implode($html);
   }
diff --git a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php
index 30edcfd2b133f71b10c795304f4a8f83866562c4..f9df3502c2722d03122eebd909686aacae2bf46a 100644
--- a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php
+++ b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php
@@ -109,7 +109,8 @@ class Intonation_Library_View_Wrapper_User_RichContent_Settings extends Intonati
       return $this->_view->renderingVertical($wrapped);
     };
 
-    return $this->_view->renderMultipleCarousel(new Storm_Collection($cards), $callback);
+    return $this->_view->renderMultipleCarousel(new Storm_Collection($cards), $callback,
+                                                (new Intonation_Library_Widget_Carousel_Settings)->setColumns(5));
   }
 
 
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Settings.php b/library/templates/Intonation/Library/Widget/Carousel/Settings.php
new file mode 100644
index 0000000000000000000000000000000000000000..dc8d09045ce69564d327199921d47c5d7ba4aa9a
--- /dev/null
+++ b/library/templates/Intonation/Library/Widget/Carousel/Settings.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Intonation_Library_Widget_Carousel_Settings {
+
+  CONST CAROUSEL_PAGE_ACTIVE = 'active';
+
+  protected int $_active_page = 0;
+  protected int $_columns = 1;
+  protected string $_classes = '';
+
+
+  public function getActivePage() : int {
+    return $this->_active_page;
+  }
+
+
+  public function setActivePage(int $page) : self {
+    $this->_active_page = $page;
+    return $this;
+  }
+
+
+  public function getColumns() : int {
+    return $this->_columns;
+  }
+
+
+  public function setColumns(int $columns) : self {
+    $this->_columns = $columns;
+    return $this;
+  }
+
+
+  public function initDefaultsFor(string $layout) : self {
+    if ($layout === Intonation_Library_Widget_Carousel_Definition::MULTIPLE_CAROUSEL)
+      $this->setColumns(3);
+
+    if ($layout === Intonation_Library_Widget_Carousel_Definition::MULTIPLE_CAROUSEL_PLUS)
+      $this->setColumns(5);
+
+    return $this;
+  }
+
+
+  public function getCssClassesForPosition(string $classes, int $position) : string {
+    if ( $position == $this->_active_page)
+      $classes .= ' ' . static::CAROUSEL_PAGE_ACTIVE;
+
+    return $classes;
+  }
+
+
+  public function setClasses(string $classes) : self {
+    $this->_classes = $classes;
+    return $this;
+  }
+
+
+  public function getClasses() : string {
+    return $this->_classes;
+  }
+}
diff --git a/library/templates/Intonation/Library/Widget/Carousel/View.php b/library/templates/Intonation/Library/Widget/Carousel/View.php
index 8c077aae298724326bc308c2f93a06f60f019cef..1b96e8f6c1a30dcdfd3db2f6d0bf8a04887e806a 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/View.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/View.php
@@ -218,7 +218,7 @@ abstract class Intonation_Library_Widget_Carousel_View extends Zendafi_View_Help
 
     return isset(static::$_helper_by_layout[$layout])
       ? static::$_helper_by_layout[$layout]
-      : Intonation_View_RenderMultipleCarousel::class;
+      : Intonation_View_RenderTruncateList::class;
   }
 
 
@@ -239,21 +239,23 @@ abstract class Intonation_Library_Widget_Carousel_View extends Zendafi_View_Help
                   Intonation_Library_Widget_Carousel_Definition::GRID]))
       $this->_layout_helper->setIdModule($this->_settings->getIdForHtml());
 
-    if ($layout === Intonation_Library_Widget_Carousel_Definition::MULTIPLE_CAROUSEL)
-      $this->_layout_helper->setNumberOfColumns(3);
-
     return $this->_layout_helper;
   }
 
 
   protected function _renderLayout($layout, $elements, $content_callback) {
-    $layout_helper = $this->_getLayoutHelper((string) $layout);
-    $helper_func = array_reverse(explode('_', get_class($layout_helper)))[0];
-    return call_user_func_array([$layout_helper,
-                                 $helper_func],
+    $args = [new Storm_Collection($elements),
+             $content_callback];
+
+    if ( $this->isLayoutCarousel((string) $layout))
+      $args [] = (new Intonation_Library_Widget_Carousel_Settings)->initDefaultsFor($layout);
+
+    $helper_class = $this->_getLayoutHelper((string) $layout);
+    $helper_function = array_reverse(explode('_', get_class($helper_class)))[0];
 
-                                [new Storm_Collection($elements),
-                                 $content_callback]);
+    return call_user_func_array([$helper_class,
+                                 $helper_function],
+                                $args);
   }
 
 
@@ -411,9 +413,10 @@ abstract class Intonation_Library_Widget_Carousel_View extends Zendafi_View_Help
   }
 
 
-  public function isLayoutCarousel() : bool {
+  public function isLayoutCarousel(string $layout = '') : bool {
+    $layout = $layout ? $layout : $this->_settings->getLayout();
     return
-      array_key_exists($this->_settings->getLayout(),
+      array_key_exists($layout,
                        (new Intonation_Library_Widget_Carousel_Definition)->getCarouselLayouts());
   }
 
diff --git a/library/templates/Intonation/View/Abonne/HistoryLoansList.php b/library/templates/Intonation/View/Abonne/HistoryLoansList.php
index a8da40b9d6cd2df1bb67053d50131234c7aa8279..c23b32bb4e463ea5ea98fc10ca17837b44d06c5d 100644
--- a/library/templates/Intonation/View/Abonne/HistoryLoansList.php
+++ b/library/templates/Intonation/View/Abonne/HistoryLoansList.php
@@ -30,6 +30,8 @@ class Intonation_View_Abonne_HistoryLoansList extends Intonation_View_Abonne_Loa
   protected function _renderList($collection, $actions) {
     return Intonation_View_RenderTruncateList::AJAX_SIZE < $collection->count()
       ? $this->view->renderTruncateList($collection, null)
-      : $this->view->renderMultipleCarousel($collection, null, 3);
+      : $this->view->renderMultipleCarousel($collection,
+                                            null,
+                                            (new Intonation_Library_Widget_Carousel_Settings)->setColumns(3));
   }
 }
diff --git a/library/templates/Intonation/View/Abstract/Carousel.php b/library/templates/Intonation/View/Abstract/Carousel.php
index 5e3775e09a8632742996534911ef582c119f746c..217fbac988340869861be392d41f3e5dc7a69568 100644
--- a/library/templates/Intonation/View/Abstract/Carousel.php
+++ b/library/templates/Intonation/View/Abstract/Carousel.php
@@ -20,18 +20,23 @@
  */
 
 
-abstract class Intonation_View_Abstract_Carousel extends Intonation_View_Abstract_Layout {
+abstract class Intonation_View_Abstract_Carousel
+  extends Intonation_View_Abstract_Layout {
 
-  protected function _renderCarousel($collection, $callback) {
+  protected Intonation_Library_Widget_Carousel_Settings $_settings;
+
+
+  protected function _renderCarousel(Storm_Collection $collection,
+                                     ?Callable $callback,
+                                     Intonation_Library_Widget_Carousel_Settings $settings) {
     if ($collection->isEmpty())
       return '';
 
+    $this->_settings = $settings;
+
     $callback = $callback
       ? $callback
-      : (function($item)
-        {
-          return $this->view->renderingVertical($item);
-        });
+      : (fn($item) => $this->view->renderingVertical($item));
 
     $id = 'carousel_' . uniqid();
 
@@ -74,18 +79,19 @@ $("#%1$s").on("slid.bs.carousel",
   }
 
 
-  protected function _indicators($count, $id) {
+  protected function _indicators(int $count, string $id) : string {
     if (1 >= $count)
       return '';
 
     $lis = [];
 
     for ($i = 0; $i < $count; $i++)
-      $lis [] = $this->_tag('li',
-                            '',
-                            ['class' => 'bg-dark ' . (($i == 0) ? 'active' : ''),
-                             'data-target' => '#' . $id,
-                             'data-slide-to' => $i]);
+      $lis [] =
+        $this->_tag('li',
+                    '',
+                    ['class' => $this->_settings->getCssClassesForPosition('bg-dark', $i),
+                     'data-target' => '#' . $id,
+                     'data-slide-to' => $i]);
 
     return $this->_tag('ol',
                        implode($lis),
@@ -93,14 +99,20 @@ $("#%1$s").on("slid.bs.carousel",
   }
 
 
-  protected function _carouselInner($collection, $id, $callback) {
-    $html = $collection->injectInto([], function($html, $element) use ($callback)
-                                    {
-                                      $html [] = $this->_tag('div',
-                                                             $callback($element),
-                                                             ['class' => 'carousel-item'. (0 == count($html) ? ' active' : '')]);
-                                      return $html;
-                                    });
+  protected function _carouselInner(Storm_Collection $collection,
+                                    string $id,
+                                    Callable $callback) : string {
+    $html = $collection
+      ->injectInto([],
+                   function($html, $element) use ($callback)
+                   {
+                     $html [] =
+                     $this->_tag('div',
+                                 $callback($element),
+                                 ['class' => ($this->_settings
+                                              ->getCssClassesForPosition('carousel-item', count($html)))]);
+                     return $html;
+                   });
 
     return $this->_tag('div',
                        implode($html),
diff --git a/library/templates/Intonation/View/Author/RenderCollaborations.php b/library/templates/Intonation/View/Author/RenderCollaborations.php
index 1b4756253283503fb5d95622e2d78eefe215b055..e7db5d43314f83df3b430eadec21fec1e8aa693d 100644
--- a/library/templates/Intonation/View/Author/RenderCollaborations.php
+++ b/library/templates/Intonation/View/Author/RenderCollaborations.php
@@ -27,7 +27,8 @@ class Intonation_View_Author_RenderCollaborations extends ZendAfi_View_Helper_Ba
     };
 
     return ($content = $this->view->renderMultipleCarousel(new Storm_Collection($author->getAssociatedAuthors()),
-                                                           $callback))
+                                                           $callback,
+                                                           (new Intonation_Library_Widget_Carousel_Settings)->setColumns(5)))
       ? $this->_tag('h3', $this->_('Auteurs associés')) . $content
       : '';
   }
diff --git a/library/templates/Intonation/View/Author/RenderRecords.php b/library/templates/Intonation/View/Author/RenderRecords.php
index 0e06c64cbfd4023b4f8f7fa193f2a8f5a12b2b1a..50760f3c4d34af64dec487f24c10f25ee0d74aad 100644
--- a/library/templates/Intonation/View/Author/RenderRecords.php
+++ b/library/templates/Intonation/View/Author/RenderRecords.php
@@ -48,10 +48,10 @@ class Intonation_View_Author_RenderRecords extends ZendAfi_View_Helper_BaseHelpe
 
     $elements = Class_Template::current()->newWrappers($records, $this->view);
 
-    $records = $this->view->renderMultipleCarousel($elements, function($record)
-                                       {
-                                         return $this->view->renderingOnlyImage($record);
-                                       });
+    $records =
+      $this->view->renderMultipleCarousel($elements,
+                                          fn($record) => $this->view->renderingOnlyImage($record),
+                                          (new Intonation_Library_Widget_Carousel_Settings)->setColumns(5));
 
     return $this->_div(['class' => 'col-12'], $header . $records);
   }
diff --git a/library/templates/Intonation/View/Author/RenderYoutubeChan.php b/library/templates/Intonation/View/Author/RenderYoutubeChan.php
index 39c5dbf5f92b1ea450e32f6225f92d9bb1eed048..ba78e22ef4dded8f5da0dabd3bc786f22c7cf4b0 100644
--- a/library/templates/Intonation/View/Author/RenderYoutubeChan.php
+++ b/library/templates/Intonation/View/Author/RenderYoutubeChan.php
@@ -49,6 +49,7 @@ class Intonation_View_Author_RenderYoutubeChan extends ZendAfi_View_Helper_BaseH
     return
       $this->_tag('h3', $this->_('Chaîne Youtube'))
       . $this->view->layoutCarousel(new Storm_Collection($items),
-                                    $callback);
+                                    $callback,
+                                    new Intonation_Library_Widget_Carousel_Settings);
   }
 }
diff --git a/library/templates/Intonation/View/RenderInterviews.php b/library/templates/Intonation/View/RenderInterviews.php
index 03089f57cdba91e8fc89306df75e51680392c541..e7982c081a118c966bec8446a535ab856e6118bc 100644
--- a/library/templates/Intonation/View/RenderInterviews.php
+++ b/library/templates/Intonation/View/RenderInterviews.php
@@ -43,6 +43,9 @@ class Intonation_View_RenderInterviews extends ZendAfi_View_Helper_BaseHelper {
                                               $interview->getDescription())));
       };
 
-    return $this->view->layoutCarousel($interviews, $callback);
+    return
+      $this->view->layoutCarousel($interviews,
+                                  $callback,
+                                  new Intonation_Library_Widget_Carousel_Settings);
   }
 }
diff --git a/library/templates/Intonation/View/RenderMultipleCarousel.php b/library/templates/Intonation/View/RenderMultipleCarousel.php
index bb156d558b891eb418349688e3394491d6476d88..8a486f7fdabb23ce589719db6c2c977e42a84386 100644
--- a/library/templates/Intonation/View/RenderMultipleCarousel.php
+++ b/library/templates/Intonation/View/RenderMultipleCarousel.php
@@ -20,28 +20,19 @@
  */
 
 
-class Intonation_View_RenderMultipleCarousel extends Intonation_View_Abstract_Carousel {
+class Intonation_View_RenderMultipleCarousel
+  extends Intonation_View_Abstract_Carousel {
 
-  protected $_columns = 5;
 
-
-  public function renderMultipleCarousel($collection, $callback = null, $number_of_columns = '') {
-    $this->setNumberOfColumns($number_of_columns);
-    return $this->_renderCarousel($collection, $callback);
-  }
-
-
-  public function setNumberOfColumns($columns) {
-    if (!$columns)
-      return $this;
-
-    $this->_columns = $columns;
-    return $this;
+  public function renderMultipleCarousel($collection,
+                                         $callback = null,
+                                         Intonation_Library_Widget_Carousel_Settings $settings) {
+    return $this->_renderCarousel($collection, $callback, $settings);
   }
 
 
   protected function _numberOfPages($collection) {
-    return ceil($collection->count() / $this->_columns);
+    return ceil($collection->count() / $this->_settings->getColumns());
   }
 
 
@@ -55,18 +46,24 @@ class Intonation_View_RenderMultipleCarousel extends Intonation_View_Abstract_Ca
   }
 
 
-  protected function _carouselInner($collection, $id, $callback) {
-    $cards = array_filter($collection->injectInto([], function($html, $element) use ($callback)
-                                                  {
-                                                    $html [] = $callback($element);
-                                                    return $html;
-                                                  }));
-
-    return parent::_carouselInner($this->_cardLayout($cards), $id, fn($element) => $element);
+  protected function _carouselInner(Storm_Collection $collection,
+                                    string $id,
+                                    Callable $callback) : string {
+    $cards =
+      array_filter($collection->injectInto([],
+                                           function($html, $element) use ($callback)
+    {
+      $html [] = $callback($element);
+      return $html;
+    }));
+
+    return parent::_carouselInner($this->_cardLayout($cards),
+                                  $id,
+                                  fn($element) => $element);
   }
 
 
   protected function _cardLayout($cards) {
-    return $this->view->renderCardLayout($cards, $this->_columns);
+    return $this->view->renderCardLayout($cards, $this->_settings->getColumns());
   }
-}
\ No newline at end of file
+}
diff --git a/library/templates/Intonation/View/RenderRecord/RenderItems.php b/library/templates/Intonation/View/RenderRecord/RenderItems.php
index 44fc6b6d4443436cbcc1210bb88ec58f2e208da9..d2a186796ffa41121e74faf7a10af2538a395bfa 100644
--- a/library/templates/Intonation/View/RenderRecord/RenderItems.php
+++ b/library/templates/Intonation/View/RenderRecord/RenderItems.php
@@ -22,11 +22,11 @@
 
 class Intonation_View_RenderRecord_RenderItems extends ZendAfi_View_Helper_BaseHelper {
 
-
   protected
     $_should_display_map_cache,
     $_should_use_ILS_items_threshold_cache,
-    $_should_paginate_cache;
+    $_should_paginate_cache,
+    $_should_display_shelf_cache;
 
 
   public function renderRecord_RenderItems(array $items, array $same_work = []) : string {
@@ -41,15 +41,46 @@ class Intonation_View_RenderRecord_RenderItems extends ZendAfi_View_Helper_BaseH
   }
 
 
+  public function renderHeadScriptsOn($script_loader) {
+    if ( !$this->_shouldDisplayShelf())
+      return $this;
+
+    Class_ScriptLoader::getInstance()
+      ->addScript(Class_Url::relative('/library/templates/Intonation/Assets/js/item_shelf/item_shelf.js'));
+
+    return $this;
+  }
+
+
   protected function _renderItems(array $items, array $same_work) : string {
-    $html = [ $this->_hookForMoreHtml($items) ];
+    $html = [$this->_renderShelf(),
+             $this->_hookForMoreHtml($items) ];
 
     return ($html = $this->_renderItemsByStrategy($items, $same_work, $html))
-      ? $this->view->grid(implode($html))
+      ? $this->view->grid($html)
       : '';
   }
 
 
+  protected function _renderShelf() : string {
+    if ( !$this->_shouldDisplayShelf())
+      return '';
+
+    $this->renderHeadScriptsOn(Class_ScriptLoader::getInstance());
+    return $this->_div(['id' => 'items_shelf',
+                        'class' => 'items_shelf']);
+  }
+
+
+  protected function _shouldDisplayShelf() : bool {
+    if ( isset($this->_should_display_shelf_cache))
+      return $this->_should_display_shelf_cache;
+
+    return $this->_should_display_shelf_cache =
+      Class_Profil_ItemsSettings::current()->isItemsShelfEnabled();
+  }
+
+
   protected function _renderItemsByStrategy(array $items,
                                             array $same_work,
                                             array $html) : array {
@@ -473,4 +504,4 @@ class Intonation_View_RenderRecord_RenderItemsStrategyMapAndILS extends
   protected function _getOsmItemWrapper() : string {
     return Intonation_Library_View_Wrapper_ItemForOsm::class;
   }
-}
\ No newline at end of file
+}
diff --git a/library/templates/Intonation/View/Search/History.php b/library/templates/Intonation/View/Search/History.php
index 41d75a1d9d2114916a17ff2a7338c5bb50933e14..0ee71fa3d1e0da204c08aad10b59d79f8da566ff 100644
--- a/library/templates/Intonation/View/Search/History.php
+++ b/library/templates/Intonation/View/Search/History.php
@@ -65,7 +65,9 @@ class Intonation_View_Search_History extends ZendAfi_View_Helper_BaseHelper {
 
     $html = [$this->view->div(['class' => 'col-12'], $this->view->renderCollectionActions($actions)),
              $this->view->div(['class' => 'col-12 mt-3'],
-                              $this->view->renderMultipleCarousel(new Storm_Collection($history), $callback))];
+                              $this->view->renderMultipleCarousel(new Storm_Collection($history),
+                                                                  $callback,
+                                                                  (new Intonation_Library_Widget_Carousel_Settings)->setColumns(5)))];
 
     $html = $this->view->grid(implode($html));
 
diff --git a/public/opac/js/ajaxifyPaginatedList/ajaxifyPaginatedList.js b/public/opac/js/ajaxifyPaginatedList/ajaxifyPaginatedList.js
index 0089b3aedaeb47b5c0cc6008bc32f0a5f5934b04..39adb27c9239fe8b4b32b153cab840be03752960 100644
--- a/public/opac/js/ajaxifyPaginatedList/ajaxifyPaginatedList.js
+++ b/public/opac/js/ajaxifyPaginatedList/ajaxifyPaginatedList.js
@@ -56,7 +56,7 @@
 
       if (undefined != $.fn.openStreetMap)
 	widget.openStreetMap();
-   
+
       initializePopups();
       setupAnchorsTarget();
     };
diff --git a/scripts/index_items_shelf_key.php b/scripts/index_items_shelf_key.php
new file mode 100644
index 0000000000000000000000000000000000000000..9cdfd1f3a602c352c2c561700832816abc2e89cb
--- /dev/null
+++ b/scripts/index_items_shelf_key.php
@@ -0,0 +1,4 @@
+<?php
+require('includes.php');
+Bokeh_Engine::getInstance()->warmUp();
+(new Class_Migration_IndexItemsShelfKey)->run();
\ No newline at end of file
diff --git a/tests/application/modules/opac/controllers/RechercheControllerTest.php b/tests/application/modules/opac/controllers/RechercheControllerTest.php
index 4f5217995b132c17a324c49e935377d95b39a51a..fba973f5b4c96ed7410ca1b4518b2900dcc0f7b0 100644
--- a/tests/application/modules/opac/controllers/RechercheControllerTest.php
+++ b/tests/application/modules/opac/controllers/RechercheControllerTest.php
@@ -1041,7 +1041,6 @@ class RechercheControllerViewNoticeClefAlphaTest
 
   /** @test */
   public function resultatSuivantShouldLinkToNavigationSuivant() {
-    xdebug_break();
     $this->assertXPathContentContains('//div//a[contains(@href, "/recherche/viewnotice/expressionRecherche/Millenium/clef/TESTINGALPHAKEY---101/id/345/navigation/suivant")]',
                                       'Document suivant');
   }
diff --git a/tests/db/UpgradeDBTest.php b/tests/db/UpgradeDBTest.php
index 5e8e8e334a5988951244bc307bc0c55bac97e114..f7ed74271d72dd052d98527defc1839228bbb5ff 100644
--- a/tests/db/UpgradeDBTest.php
+++ b/tests/db/UpgradeDBTest.php
@@ -27,7 +27,7 @@ abstract class UpgradeDBTestCase extends PHPUnit_Framework_TestCase {
     $this->_movePatchLevelTo(min($patch_level, $this->_getPatchLevel() - 1));
     $this->prepare();
     $migrator = (new Class_Migration_ScriptPatchs());
-    $migrator->setEcho(function(){});
+    $migrator->setEchoFunction(fn() => null);
     $migrator->runTo($this->_getPatchLevel());
   }
 
@@ -4630,3 +4630,38 @@ class UpgradeDB_433_Test extends UpgradeDBTestCase {
     $this->assertFieldType('codif_type_doc', 'label_zotero', 'varchar(255)');
   }
 }
+
+
+
+
+class UpgradeDB_434_Test extends UpgradeDBTestCase {
+
+  public function prepare() {
+    $this->silentQuery("alter table exemplaires drop column shelf_key");
+    $this->silentQuery("alter table exemplaires drop column shelf_key_update_date");
+  }
+
+
+  /** @test */
+  public function tableExemplairesShouldHaveColumnShelfKeyWithTypeVarchar255() {
+    $this->assertFieldType('exemplaires', 'shelf_key', 'varchar(255)');
+  }
+
+
+  /** @test */
+  public function shelfKeyDefaultShouldBeNull() {
+    $this->assertFieldNullable('exemplaires', 'shelf_key');
+  }
+
+
+  /** @test */
+  public function tableExemplairesShouldHaveColumnShelfKeyUpdateDateWithTypeTimestamp() {
+    $this->assertFieldType('exemplaires', 'shelf_key_update_date', 'timestamp');
+  }
+
+
+  /** @test */
+  public function shelfKeyUpdateDateDefaultShouldBeNull() {
+    $this->assertFieldNullable('exemplaires', 'shelf_key_update_date');
+  }
+}
\ No newline at end of file
diff --git a/tests/js/ItemShelf.php b/tests/js/ItemShelf.php
new file mode 100644
index 0000000000000000000000000000000000000000..af51ac915f098e0aaa75e9b1468c0ea98c6ef96f
--- /dev/null
+++ b/tests/js/ItemShelf.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class ItemShelf extends BrowserTest {
+  protected $_test_path = 'library/templates/Intonation/Assets/js/item_shelf/item_shelf_tests.html';
+}
diff --git a/tests/library/Class/BatchTest.php b/tests/library/Class/BatchTest.php
index ad75d4484045dad1f3dbaa39d115c6e13d3d2648..142a7ed7268e3b9a15cf0dcf8199525874099733 100644
--- a/tests/library/Class/BatchTest.php
+++ b/tests/library/Class/BatchTest.php
@@ -123,12 +123,13 @@ class BatchLoaderWithoutRessourcesNumeriquesTest extends ModelTestCase {
 
 class BatchIndexRessourcesNumeriquesTest extends ModelTestCase {
   public function setUp() {
+    parent::setUp();
     $album = $this->fixture('Class_Album', ['id' => 1,
                                             'titre' => 'Mon Album',
                                             'status' => Class_Album::STATUS_VALIDATED]);
-    Class_Notice::beVolatile();
 
     Storm_Cache::beVolatile();
+
     $this->cache = new Storm_Cache();
     $this->cache->save('some', 'data');
     $this->assertEquals('some', $this->cache->load('data'));
diff --git a/tests/library/Class/Cosmogramme/Integration/PhaseItemsShelfKeyGenerationTest.php b/tests/library/Class/Cosmogramme/Integration/PhaseItemsShelfKeyGenerationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3491e5975b8a0d0e73168bc911687d6c07297ab1
--- /dev/null
+++ b/tests/library/Class/Cosmogramme/Integration/PhaseItemsShelfKeyGenerationTest.php
@@ -0,0 +1,202 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+abstract class PhaseItemsShelfKeyGenerationTestCase extends Class_Cosmogramme_Integration_PhaseTestCase {
+
+  protected bool $_build_result_to_update_done = false;
+  protected bool $_build_result_to_init_done = false;
+
+
+  public function setUp() {
+    parent::setUp();
+
+    Class_Cosmogramme_Integration_PhaseAbstract::shouldThrowError(true);
+    Class_CosmoVar::setValueOf('shelf_key_update_date', '2022-05-31 10:45:12');
+    Class_Migration_IndexItemsShelfKey::setTimeSource(new TimeSourceForTest('2022-06-01 03:07:10'));
+
+    $count = 3;
+
+    $sql = $this
+      ->mock()
+      ->beStrict()
+
+      ->whenCalled('fetchAllByColumn')
+      ->with("select count(exemplaires.id) from exemplaires where exemplaires.cote > '' AND ( exemplaires.shelf_key IS NULL OR exemplaires.shelf_key = '' OR ( exemplaires.shelf_key_update_date > '2022-05-31 10:45:12' AND exemplaires.shelf_key_update_date < '2022-06-01 03:07:10' ))")
+      ->answers([$count])
+
+      ->whenCalled('fetchAll')
+      ->with("select exemplaires.id, exemplaires.id_notice, exemplaires.cote, notices.clef_chapeau, notices.tome_alpha, notices.clef_alpha from exemplaires inner join notices on exemplaires.id_notice = notices.id_notice where exemplaires.cote > '' AND ( exemplaires.shelf_key IS NULL OR exemplaires.shelf_key = '' OR ( exemplaires.shelf_key_update_date > '2022-05-31 10:45:12' AND exemplaires.shelf_key_update_date < '2022-06-01 03:07:10' )) limit 5000")
+      ->willDo(fn() => $this->_buildResultToUpdate($count))
+
+      ->whenCalled('execute')
+      ->with('update exemplaires set shelf_key = (CASE WHEN (id = "1") THEN "0AMT_000R_0GER_0740_0001_0698_BOOK-OF-_BOOK_BOOK-OF-_0001" WHEN (id = "2") THEN "0AMT_000R_0GER_0740_0001_0698_BOOK-OF-_BOOK_BOOK-OF-_0002" WHEN (id = "3") THEN "0AMT_000R_0GER_0740_0001_0698_BOOK-OF-_BOOK_BOOK-OF-_0003" ELSE shelf_key END), shelf_key_update_date = \'2022-06-01 03:07:10\' WHERE id in (1,2,3)')
+      ->answers(true)
+
+      ->whenCalled('execute')
+      ->with('update exemplaires set shelf_key = (CASE WHEN (id = "1") THEN "0AMT_000R_0GER_0740_0001_0698_BOOK-OF-_BOOK_BOOK-OF-_0001" WHEN (id = "2") THEN "0AMT_000R_0GER_0740_0001_0698_BOOK-OF-_BOOK_BOOK-OF-_0002" ELSE shelf_key END), shelf_key_update_date = \'2022-06-01 03:07:10\' WHERE id in (1,2,3)')
+      ->answers(true);
+
+    Zend_Registry::set('sql', $sql);
+
+    $this->_phase = $this->_buildPhase('ItemsShelfKeyGeneration')
+                         ->noDbReset()
+                         ->run();
+  }
+
+
+  public function tearDown() {
+    Class_Cosmogramme_Integration_PhaseAbstract::shouldThrowError(false);
+    parent::tearDown();
+  }
+
+
+  protected function _buildResultToUpdate(int $count) : array {
+    if ( $this->_build_result_to_update_done)
+      return [];
+
+    $this->_build_result_to_update_done = true;
+    return $this->_buildResult($count);
+  }
+
+
+  protected function _buildResultToInit(int $count) : array {
+    if ( $this->_build_result_to_init_done)
+      return [];
+
+    $this->_build_result_to_init_done = true;
+    return $this->_buildResult($count);
+  }
+
+
+  protected function _buildResult(int $count) : array {
+    $result = [];
+    for ($i = 1; $i <= $count ; $i++)
+      $result [] =
+        ['id' => $i,
+         'id_notice' => $i,
+         'cote' => 'AMT R GER 740.1 698',
+         'clef_chapeau' => 'BOOK-OF-SOULS',
+         'tome_alpha' => 'BOOK_1',
+         'clef_alpha' => 'BOOK-OF-SOULS_1_2_3'];
+
+    return $result;
+  }
+}
+
+
+
+
+class PhaseItemsShelfKeyGenerationBadPreviousPhaseTest extends PhaseItemsShelfKeyGenerationTestCase {
+
+  protected function _getPreviousPhase() {
+    return new Class_Cosmogramme_Integration_Phase(7);
+  }
+
+
+  /** @test */
+  public function shouldNotChangePhase() {
+    $this->assertTrue($this->_phase->isId(7));
+  }
+}
+
+
+
+
+class PhaseItemsShelfKeyGenerationRunTest extends PhaseItemsShelfKeyGenerationTestCase {
+
+  protected function _getPreviousPhase() {
+    return (new Class_Cosmogramme_Integration_Phase(7.3))
+      ->beCron();
+  }
+
+
+  /** @test */
+  public function phaseShouldBeSevenDotThree() {
+    $this->assertTrue($this->_phase->isId(7.3));
+  }
+
+
+  /** @test */
+  public function logShouldContainsUIEu() {
+    $this->assertEquals(' ===== Début de la phase d\'indexation des clés d\'étagères des exemplaires =====
+
+ ===== Indexation des clés =====
+
+ Nombre de clés à indexer : 3
+
+ page de 3 exemplaires : 1/1
+  en 0s
+
+ [OK]
+
+ ===== Indexation des clés traitée en 0s =====
+
+
+ ===== Fin de la phase d\'indexation des clés d\'étagères. Durée : 0s =====
+
+
+', $this->_log_content);
+  }
+}
+
+
+
+
+class PhaseItemsShelfKeyGenerationRunWithMysqlLoopTest extends PhaseItemsShelfKeyGenerationTestCase {
+
+  protected function _getPreviousPhase() {
+    return (new Class_Cosmogramme_Integration_Phase(7.3))
+      ->beCron();
+  }
+
+
+  /** @test */
+  public function phaseShouldBeSevenDotThree() {
+    $this->assertTrue($this->_phase->isId(7.3));
+  }
+
+
+  protected function _buildResultToUpdate(int $count) : array {
+    return $this->_buildResult(3);
+  }
+
+
+  /** @test */
+  public function logShouldContainsUIEu() {
+    $this->assertEquals(' ===== Début de la phase d\'indexation des clés d\'étagères des exemplaires =====
+
+ ===== Indexation des clés =====
+
+ Nombre de clés à indexer : 3
+
+ page de 3 exemplaires : 1/1
+  en 0s
+
+ ===== Indexation des clés stopée. =====
+
+
+ ===== Fin de la phase d\'indexation des clés d\'étagères. Durée : 0s =====
+
+
+', $this->_log_content);
+  }
+}
\ No newline at end of file
diff --git a/tests/library/Class/ExemplaireTest.php b/tests/library/Class/ExemplaireTest.php
index e681cc3dd25abcd18e3fa710bfdf72032559c074..1e2ac8ddbd9de262d6a8e13c148b77799fd8fbe7 100644
--- a/tests/library/Class/ExemplaireTest.php
+++ b/tests/library/Class/ExemplaireTest.php
@@ -21,10 +21,6 @@
 
 
 class Class_ExemplaireTest extends ModelTestCase {
-  public function setUp() {
-    parent::setUp();
-
-  }
 
   /** @test */
   public function exemplaireIsRessourceNumeriqueShouldNotBeReservable() {
@@ -32,6 +28,7 @@ class Class_ExemplaireTest extends ModelTestCase {
     $this->assertFalse($exemplaire->isReservable());
   }
 
+
   /** @test */
   public function exemplaireIsSigbExemplaireAndIsNotReservableShouldNotBeReservable() {
     $sigb_exemplaire= Storm_Test_ObjectWrapper::mock()
@@ -70,4 +67,4 @@ class Class_ExemplaireTest extends ModelTestCase {
 
     $this->assertTrue($item->isReservable());
   }
-}
\ No newline at end of file
+}
diff --git a/tests/library/Class/Migration/UpdateConfigTest.php b/tests/library/Class/Migration/UpdateConfigTest.php
index a24991e26b7a938332dd180c55e813982b8d9a25..4bd16268e29988ee99929f642e96013007ca8873 100644
--- a/tests/library/Class/Migration/UpdateConfigTest.php
+++ b/tests/library/Class/Migration/UpdateConfigTest.php
@@ -21,8 +21,6 @@
 
 
 class UpdateConfigFileTest extends ModelTestCase {
-  protected $_storm_default_to_volatile = true;
-
 
   public function setUp() {
     parent::setUp();
@@ -49,9 +47,8 @@ class UpdateConfigFileTest extends ModelTestCase {
       ->answers(true);
 
     Class_Migration_UpdateConfig::setFileSystem($file_system);
-    Class_Migration_UpdateConfig::setEcho(true);
 
-    (new Class_Migration_UpdateConfig)->run('bokeh_test_db');
+    (new Class_Migration_UpdateConfig)->runWithoutEcho('bokeh_test_db');
   }
 
 
diff --git a/tests/library/Class/Migration/UpdateDatabaseAfterSelectDbTest.php b/tests/library/Class/Migration/UpdateDatabaseAfterSelectDbTest.php
index c144936745439f7d286823d16d0a600690fc8d58..d66bd4332611da55dd3640ed743f6a4293af5d7f 100644
--- a/tests/library/Class/Migration/UpdateDatabaseAfterSelectDbTest.php
+++ b/tests/library/Class/Migration/UpdateDatabaseAfterSelectDbTest.php
@@ -37,8 +37,7 @@ class UpdateDatabaseAfterSelectDbTest extends ModelTestCase {
     Class_AdminVar::set('STATUS_REPORT_PUSH_URL', 'https://git-bokeh.org');
     Class_AdminVar::set('FORCE_HTTPS', '1');
 
-    Class_Migration_UpdateDatabaseAfterSelectDb::setEcho(true);
-    (new Class_Migration_UpdateDatabaseAfterSelectDb)->run();
+    (new Class_Migration_UpdateDatabaseAfterSelectDb)->runWithoutEcho();
   }
 
 
diff --git a/tests/library/Class/MigrationTest.php b/tests/library/Class/MigrationTest.php
index c3492169e1e27d7252abde58f96f7f212f077ce4..48941bdcac7a5431ef9f481ea1ea5d4fd7919175 100644
--- a/tests/library/Class/MigrationTest.php
+++ b/tests/library/Class/MigrationTest.php
@@ -25,8 +25,6 @@ abstract class MigrationTestCase extends ModelTestCase {
   public function setUp() {
     parent::setUp();
     Storm_Model_Loader::defaultToVolatile();
-    Class_Migration_ScriptPatchs::setEcho('none');
-
     $this->fixture('Class_CosmoVar', ['id' => 'patch_level',
                                       'valeur' => 10 ] );
     $this->_old_sql = Zend_Registry::get('sql');
@@ -92,21 +90,23 @@ class Class_MigrationScriptPatchsTest extends MigrationTestCase {
 
   /** @test */
   public function runPatchShouldUpgradePatchLevelTo12() {
-    (new Class_Migration_ScriptPatchs())->run();
+    (new Class_Migration_ScriptPatchs())->runWithoutEcho();
     $this->assertEquals(12,Class_CosmoVar::find('patch_level')->getValeur());
   }
 
 
   /** @test */
   public function runForcePatchShouldUpgradePatchLevelTo14() {
-    (new Class_Migration_ScriptPatchs())->run(true);
+    (new Class_Migration_ScriptPatchs())->runWithoutEcho(true);
     $this->assertEquals(14, Class_CosmoVar::find('patch_level')->getValeur());
   }
 
 
   /** @test */
   public function runFromPatchShouldUpgradePatchLevelTo14() {
-    (new Class_Migration_ScriptPatchs())->runFrom(1, true);
+    (new Class_Migration_ScriptPatchs())
+      ->setEchoFunction(function() {})
+      ->runFrom(1, true);
     $this->assertEquals(14, Class_CosmoVar::find('patch_level')->getValeur());
   }
 }
@@ -122,21 +122,21 @@ class MigrationScriptPatchsPhpTest extends MigrationTestCase {
 
   /** @test */
   public function runPatchShouldUpgradePatchLevelTo12() {
-    (new Class_Migration_ScriptPatchs())->run();
+    (new Class_Migration_ScriptPatchs())->runWithoutEcho();
     $this->assertEquals(12, Class_CosmoVar::find('patch_level')->getValeur());
   }
 
 
   /** @test */
   public function runForcePatchShouldUpgradePatchLevelTo14() {
-    (new Class_Migration_ScriptPatchs())->run(true);
+    (new Class_Migration_ScriptPatchs())->runWithoutEcho(true);
     $this->assertEquals(15, Class_CosmoVar::find('patch_level')->getValeur());
   }
 
 
   /** @test */
   public function runShouldSaveSha1() {
-    (new Class_Migration_ScriptPatchs())->run();
+    (new Class_Migration_ScriptPatchs())->runWithoutEcho();
     $this->assertEquals('bidonquiapaslatetedunchathein',
                         Class_Migration_PatchHash::findFirstBy([])->getValue());
   }
@@ -144,7 +144,7 @@ class MigrationScriptPatchsPhpTest extends MigrationTestCase {
 
   /** @test */
   public function runForcedShouldSaveTwoSha1() {
-    (new Class_Migration_ScriptPatchs())->run(true);
+    (new Class_Migration_ScriptPatchs())->runWithoutEcho(true);
     $this->assertEquals(2, Class_Migration_PatchHash::count());
   }
 }
@@ -160,8 +160,7 @@ class MigrationFunctionSQLTest extends MigrationTestCase {
 
   /** @test */
   public function withSqlFunctionShouldRunPatchUntil14() {
-    (new Class_Migration_ScriptPatchs())->run(false);
+    (new Class_Migration_ScriptPatchs())->runWithoutEcho(false);
     $this->assertEquals(14, Class_CosmoVar::find('patch_level')->getValeur());
   }
-}
-?>
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/tests/scenarios/ShelfNavigation/ShelfNavigationTest.php b/tests/scenarios/ShelfNavigation/ShelfNavigationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e5afbf52bdd1b0786b01716d16e7a2c822009ca
--- /dev/null
+++ b/tests/scenarios/ShelfNavigation/ShelfNavigationTest.php
@@ -0,0 +1,476 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class ShelfNavigationShelfKeyTest extends ModelTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 1,
+                    'cote' => '743.12 BD A']);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 11,
+                    'cote' => 'l\'écureuil volant']);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 111,
+                    'cote' => 'AMT R GER 740.1 698']);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 3456780,
+                    'cote' => 'AMT R GER 740.1 698 I AM LONG']);
+
+    $this->fixture(Class_Notice::class,
+                   ['id' => 1,
+                    'tome_alpha' => '',
+                    'clef_alpha' => 'TITREALPHA1',
+                    'clef_chapeau' => '',
+                    'exemplaires' => [Class_Exemplaire::find(1),
+                                      Class_Exemplaire::find(11)]]);
+
+    $this->fixture(Class_Notice::class,
+                   ['id' => 2,
+                    'tome_alpha' => '365',
+                    'clef_alpha' => 'OTHERTITLE',
+                    'clef_chapeau' => 'KEYCHAPEAU',
+                    'exemplaires' => [Class_Exemplaire::find(111),
+                                      Class_Exemplaire::find(3456780)]]);
+  }
+
+
+  public function getShelfKeyByIdItem() {
+    return [
+            ['0743_0012_00BD_000A_0000_0000_00000000_0000_TITREALP_0001', 1],
+            ['000L_ECUR_VOLA_0000_0000_0000_00000000_0000_TITREALP_0011', 11],
+            ['0AMT_000R_0GER_0740_0001_0698_KEYCHAPE_0365_OTHERTIT_0111', 111],
+            ['0AMT_000R_0GER_0740_0001_0698_KEYCHAPE_0365_OTHERTIT_6780', 3456780],
+    ];
+  }
+
+
+  /**
+   * @dataProvider getShelfKeyByIdItem
+   * @test
+   */
+  public function forItemIdShouldCreateExpectedShelfKey($shelf_key, $id_item) {
+    $this->assertEquals($shelf_key,
+                        (Class_Exemplaire::find($id_item)
+                         ->initShelfKey()
+                         ->getShelfKey()));
+  }
+}
+
+
+
+
+abstract class ShelfNavigationSearchShelfTestCase
+  extends AbstractControllerTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->_buildTemplateProfil(['id' => 1,
+                                 'libelle' => 'Shelf templates']);
+
+    (new Class_Profil_ItemsSettings(Class_Profil::find(1)))
+      ->setSettings(['enable_items_shelf' => 1]);
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 1,
+                    'libelle' => 'Annecy']);
+
+    $builder = (new class() {
+      use Storm_Test_THelpers;
+
+      protected $_id = 0;
+
+      public function buildRecord(int $id_bib,
+                                  int $id_emplacement,
+                                  int $id_section) {
+        $this->_id++;
+
+        $exemplaire =
+          $this->fixture(Class_Exemplaire::class,
+                         ['id' => $this->_id,
+                          'id_bib' => $id_bib,
+                          'section' => $id_section,
+                          'emplacement' => $id_emplacement,
+                          'cote' => 'COT 743.12 BD 0' . $this->_id
+                         ])
+               ->initShelfKey();
+        $exemplaire->save();
+
+        $exemplaire_bis =
+          $this->fixture(Class_Exemplaire::class,
+                         ['id' => $this->_id + 100,
+                          'id_bib' => $id_bib,
+                          'section' => $id_section,
+                          'emplacement' => $id_emplacement,
+                          'cote' => 'COT 743.12 BD 0' . $this->_id
+                         ])
+               ->initShelfKey();
+        $exemplaire_bis->save();;
+
+        $this->fixture(Class_Notice::class,
+                       ['id' => $this->_id,
+                        'type_doc' => 1,
+                        'titre_principal' => 'Titre ' . $this->_id,
+                        'exemplaires' => [ $exemplaire, $exemplaire_bis ]
+                       ]);
+      }
+    });
+
+    for($i = 0; $i < 30; $i++)
+      $builder->buildRecord(1, 10, 100);
+
+    $before = $builder->buildRecord(1, 9, 99);
+    $after  = $builder->buildRecord(1, 66, 666);
+  }
+}
+
+
+
+
+class ShelfNavigationSearchShelfInCenterTest
+  extends ShelfNavigationSearchShelfTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->dispatch('/recherche/shelf/item_id/15');
+  }
+
+
+  public function itemTitleAndPosition() {
+    return [
+            ['Titre 13', 1],
+            ['Titre 14', 2],
+            ['Titre 15', 3],
+            ['Titre 16', 4],
+            ['Titre 17', 5],
+    ];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider itemTitleAndPosition
+   **/
+  public function activePageInMiddleShouldContainsCardTitleForTitleAtPostion($title, $position) {
+    $this->assertXPath(sprintf('//div[contains(@class, "carousel-item")][3][contains(@class, "active")]//div[contains(@class, "card_with_overlay")][%s]//div[@class="card-title"][text()="%s"]',
+                               $position,
+                               $title));
+  }
+
+
+  /** @test */
+  public function selectItem15ShouldRenderInDivCardWithOverlayAndShelfCurrentItem() {
+    $this->assertXPathContentContains('//div[@class="carousel-item active"]//div[contains(@class, "card_with_overlay")][3]/@class',
+                                      'shelf_current_item');
+  }
+
+
+  /** @test */
+  public function onlyOneCardShouldHaveClassShelfCurrentItem() {
+    $this->assertXPathCount('//div[@class="card_with_overlay shelf_current_item card text-center"]', 1);
+  }
+
+
+  /** @test */
+  public function carouselIndicatorsShouldBeOnThirdPage() {
+    $this->assertXPath('//ol[@class="carousel-indicators my-2 position-relative"]/li[3][contains(@class, "active")]');
+  }
+
+
+  /** @test */
+  public function carouselShouldHaveFivePagesWithBodyPage() {
+    $this->assertXPathCount('//body//main//div[contains(@class, "carousel-item")]',
+                            5);
+  }
+}
+
+
+
+
+class ShelfNavigationSearchShelfAtEndTest extends ShelfNavigationSearchShelfTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    $this->dispatch('/recherche/shelf/item_id/29');
+  }
+
+
+  public function itemTitleAndPosition() {
+    return [
+            ['Titre 27', 1],
+            ['Titre 28', 2],
+            ['Titre 29', 3],
+            ['Titre 30', 4],
+    ];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider itemTitleAndPosition
+   **/
+  public function activePageInMiddleShouldContainsCardTitleForTitleAtPostion($title, $position) {
+    $this->assertXPath(sprintf('//div[contains(@class, "carousel-item")][3][contains(@class, "active")]//div[contains(@class, "card_with_overlay")][%s]//div[contains(@class, "card-title")][text()="%s"]',
+                               $position,
+                               $title));
+  }
+
+
+  /** @test */
+  public function activePageShouldContainsOnlyFourItems() {
+    $this->assertXPathCount('//div[contains(@class, "carousel-item active")]//div[contains(@class, "card_with_overlay")]',
+                            4);
+  }
+}
+
+
+
+
+class ShelfNavigationSearchShelfAtStartTest
+  extends ShelfNavigationSearchShelfTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->dispatch('/recherche/shelf/item_id/2');
+  }
+
+
+  public function itemTitleAndPosition() {
+    return [
+            ['Titre 1', 1],
+            ['Titre 2', 2],
+            ['Titre 3', 3],
+            ['Titre 4', 4],
+            ['Titre 5', 5],
+    ];
+  }
+
+  /**
+   * @test
+   * @dataProvider itemTitleAndPosition
+   **/
+  public function activePageAtStartShouldContainsCardTitleForTitleAtPostion($title, $position) {
+    $this->assertXPath(sprintf('//div[contains(@class, "carousel-item")][1][contains(@class, "active")]//div[contains(@class, "card_with_overlay")][%s]//div[contains(@class, "card-title")][text()="%s"]',
+                               $position,
+                               $title));
+  }
+
+
+  /** @test */
+  public function pageShouldContainsH2WithEtagereDeLabibliothequeAnnecy() {
+    $this->assertXPathContentContains('//h2', 'Étagère de la bibliothèque Annecy');
+  }
+}
+
+
+
+
+class ShelfNavigationSearchShelfRenderAjaxTest
+  extends ShelfNavigationSearchShelfTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->dispatch('/recherche/shelf/item_id/4/render/ajax');
+  }
+
+
+  /** @test */
+  public function carouselShouldRenderWithoutLayout() {
+    $this->assertNotXPath('//body//main');
+  }
+
+
+  /** @test */
+  public function responseShouldContainsCarousel() {
+    $this->assertXPathCount('//div[@class="carousel-item active"][1]//div[contains(@class, "card_with_overlay")]', 5, $this->_response->getBody());
+  }
+}
+
+
+
+
+class ShelfNavigationSearchErrorTest
+  extends ShelfNavigationSearchShelfTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->dispatch('/recherche/shelf/item_id/1004/render/ajax', false);
+  }
+
+
+  /** @test */
+  public function responseCodeShouldBe404() {
+    $this->assertResponseCode(404);
+  }
+}
+
+
+
+
+class ShelfNavigationWithoutEnableItemsShelfOptionTest
+  extends ShelfNavigationSearchShelfTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    (new Class_Profil_ItemsSettings(Class_Profil::find(1)))->setSettings(['enable_items_shelf' => 0]);
+
+    $this->dispatch('/noticeajax/resources/id/4');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkBrowseShelf() {
+    $this->assertNotXPath('//a[contains(@class, "browse-shelf")]');
+  }
+}
+
+
+
+
+class ShelfNavigationSearchNoticeAjaxResourcesTest
+  extends ShelfNavigationSearchShelfTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->dispatch('/noticeajax/resources/id/4');
+  }
+
+
+  /** @test */
+  public function itemShouldContainsLinkToRechercheShelfItemId104RenderAjax() {
+    $this->assertXPath('//div[@class="badge-group badge_group badge_group_Intonation_Library_View_Wrapper_ItemWithoutSIGB"]//a[@class="badge_tag browse_shelf text-left badge badge-secondary"][contains(@href, "/recherche/shelf/item_id/4")][contains(@title, "Parcourir")][@onclick="$(this).item_shelf(event);"]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsDivItemsShelf() {
+    $this->assertXPath('//div[@class="container-fluid"]/div[@class="row no-gutters"]/div[@id="items_shelf"][@class="items_shelf col-12"]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsScriptFileItemShelf() {
+    $this->assertXPath('//script[contains(@src, "/library/templates/Intonation/Assets/js/item_shelf/item_shelf.js")]');
+  }
+}
+
+
+
+
+class ShelfNavigationPNBTest extends AbstractControllerTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->_buildTemplateProfil(['id' => 1,
+                                 'libelle' => 'Shelf templates']);
+
+    $exemplaire = $this->fixture(Class_Exemplaire::class,
+                                 ['id' => 1,
+                                  'id_bib' => 1,
+                                  'section' => 1,
+                                  'emplacement' => 1,
+                                  'cote' => 'COT 743.12 BD 001'
+                                 ]);
+
+    $this->fixture(Class_Notice::class,
+                   ['id' => 1,
+                    'type_doc' => 112,
+                    'titre_principal' => 'Titre PNB',
+                    'exemplaires' => [ $exemplaire ]
+                   ]);
+
+    $this->dispatch('/noticeajax/resources/id/1');
+  }
+
+
+  /** @test */
+  public function itemShouldNotContainsShelfLink() {
+    $this->assertNotXPath('//a[contains(@href, "/recherche/shelf/item_id")]');
+  }
+}
+
+
+
+
+class ShelfNavigationAdminTest extends Admin_AbstractControllerTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->_buildTemplateProfil(['id' => 1,
+                                 'libelle' => 'Shelf templates']);
+
+    $this->dispatch('/admin/modulesnotice/exemplaires/id_profil/1');
+  }
+
+
+  /** @test */
+  public function formShouldContainsCheckboxEnableItemsShelf() {
+    $this->assertXPath('//form//input[@type="checkbox"][@name="enable_items_shelf"]');
+  }
+}
+
+
+
+
+class ShelfNavigationWithNoShelKeyTest extends ShelfNavigationSearchShelfTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $exemplaire_with_no_shelf_key =
+      $this->fixture(Class_Exemplaire::class,
+                     ['id' => 789798,
+                      'id_bib' => 1,
+                      'section' => 1,
+                      'emplacement' => 1,
+                      'cote' => 'COT 743.12 BD 001']);
+
+    $this->fixture(Class_Notice::class,
+                   ['id' => 1781723498,
+                    'type_doc' => 1,
+                    'titre_principal' => 'Titre sans shelf_key',
+                    'exemplaires' => [$exemplaire_with_no_shelf_key]
+                   ]);
+
+    $this->dispatch('/noticeajax/resources/id/1781723498');
+  }
+
+
+  /** @test */
+  public function itemWithNoShelfKeyShouldNotHaveShelfLink() {
+    $this->assertNotXPath('//a[contains(@href, "/recherche/shelf/item_id/789798")]');
+  }
+}
\ No newline at end of file
diff --git a/tests/scenarios/Templates/ChiliMultipleCarouselTest.php b/tests/scenarios/Templates/ChiliMultipleCarouselTest.php
index c2c23ead57da45fa0fc99fc77f4d4e5071cc1ba0..76410cbf47f7ad11bca574af827cddad67da23a5 100644
--- a/tests/scenarios/Templates/ChiliMultipleCarouselTest.php
+++ b/tests/scenarios/Templates/ChiliMultipleCarouselTest.php
@@ -21,8 +21,6 @@
 
 
 class ChiliMultipleCarouselResponsiveTest extends AbstractControllerTestCase {
-  protected $_storm_default_to_volatile = true;
-
 
   public function setUp() {
     parent::setUp();
@@ -91,7 +89,7 @@ class ChiliMultipleCarouselResponsiveTest extends AbstractControllerTestCase {
 
   /** @test */
   public function bottenShouldBeInSmActive() {
-    $this->assertXPathContentContains('//div[contains(@class, "col-12 d-block d-md-none")]//div[contains(@class, "carousel")]//div[@class="carousel-item active"]',
-                                      'Botten', $this->_response->getBody());
+    $this->assertXPathContentContains('//div[@class="col-12 d-block d-md-none"]//div[@class="carousel slide"]//div[@class="carousel-item active"]',
+                                      'Botten');
   }
 }
diff --git a/tests/scenarios/Templates/ChiliTest.php b/tests/scenarios/Templates/ChiliTest.php
index 0f1457586d1c1ee5a5d7ab1375cfebe56b3eb63a..9a6df4e7eee0b0d967d47fbba8d0b8077207ec2d 100644
--- a/tests/scenarios/Templates/ChiliTest.php
+++ b/tests/scenarios/Templates/ChiliTest.php
@@ -357,7 +357,7 @@ class ChiliTemplateUpdateSettingsTest extends ChiliTemplateTestCase {
     $chili_settings
       ->setSettings(serialize($chili_settings_instance->toArray()))->save();
 
-    (new Class_Template_Update)->runWithEcho(true);
+    (new Class_Template_Update)->runWithoutEcho();
     $chili_settings->clearCache();
   }
 
diff --git a/tests/scenarios/Templates/MuscleTemplateTest.php b/tests/scenarios/Templates/MuscleTemplateTest.php
index cb3a6e08dc7f3d7b5e5fa9f911569612163b6f0f..41f844b8b9b91049072e599e84c89b9dc3d9230e 100644
--- a/tests/scenarios/Templates/MuscleTemplateTest.php
+++ b/tests/scenarios/Templates/MuscleTemplateTest.php
@@ -418,8 +418,7 @@ class MuscleTemplateUpdateSettingsTest extends MuscleTemplateTestCase {
 
     $muscle_settings->setSettings(serialize($muscle_settings_instance->toArray()))->save();
     $updader = (new Class_Template_Update);
-    $updader->setEcho(true);
-    $updader->run();
+    $updader->runWithoutEcho();
     $muscle_settings->clearCache();
   }
 
diff --git a/tests/scenarios/Templates/TemplatesLibraryTest.php b/tests/scenarios/Templates/TemplatesLibraryTest.php
index 8172832853ff4d8d3184bcf986d8b9e5d4e08274..6229c981f4fd2b969b3e8ea1325a1068fedff6f1 100644
--- a/tests/scenarios/Templates/TemplatesLibraryTest.php
+++ b/tests/scenarios/Templates/TemplatesLibraryTest.php
@@ -631,7 +631,7 @@ class TemplatesLibraryWidgetWithOSMAndLinkToProfileTest extends TemplatesIntonat
     $settings_insance
       ->setIntonationIconsMapUtils(['osm_closed_marker' => 'uri /map/were-closed.png']);
     $settings->setSettings(serialize($settings_insance->toArray()))->save();
-    (new Class_Template_Update)->runWithEcho(true);
+    (new Class_Template_Update)->runWithoutEcho();
     $settings->clearCache();
 
     $this->dispatch('/opac/index/index/id_profil/72');
diff --git a/tests/scenarios/Templates/TemplatesSearchItemsTest.php b/tests/scenarios/Templates/TemplatesSearchItemsTest.php
index 954782ec404a852bce81ca38d5ee69ef2c598b2f..fe38c886333ce399983145e1f1c670daba99a050 100644
--- a/tests/scenarios/Templates/TemplatesSearchItemsTest.php
+++ b/tests/scenarios/Templates/TemplatesSearchItemsTest.php
@@ -236,6 +236,18 @@ class TemplateSearchItemsResourceWith30orLessItemsTests
   public function with30ItemsShouldNotDisplaySearchForm() {
     $this->assertNotXPath('//input[contains(@class,"zendafi_form_ajax_search")]');
   }
+
+
+  /** @test */
+  public function itemsShelfShouldNotBePresent() {
+    $this->assertNotXPath('//div[@id="items_shelf"]');
+  }
+
+
+  /** @test */
+  public function itemsShelfScriptShouldNotBePresent() {
+    $this->assertNotXPath('//script[contains(@src, "item_shelf")]');
+  }
 }
 
 
@@ -654,6 +666,7 @@ class TemplatesSearchItemsModulesNoticeExemplairesActionTest extends TemplatesTe
             [ 'input[@id="emplacement"][@type="checkbox"]' ],
             [ 'input[@id="date_retour"][@type="checkbox"]' ],
             [ 'input[@id="pagination_threshold"][@type="number"][@value="200"]' ],
+            [ 'input[@id="enable_items_shelf"][@type="checkbox"]' ],
     ];
   }
 
@@ -1144,4 +1157,4 @@ class TemplatesSearchItems15ItemsPaginationThreshold20AndAvailabilityKohaLimit20
   public function jqueryShouldNotBeNotBeLoaded() {
     $this->assertNotXPath('//script[contains(@src, "jquery")]');
   }
-}
\ No newline at end of file
+}
diff --git a/tests/scenarios/Templates/TemplatesWidgetCarouselTest.php b/tests/scenarios/Templates/TemplatesWidgetCarouselTest.php
index a7a3e0c491f535a9639aec982063e455990e2e9b..db304a2a918e9bb7c811dd14c6a10853536af3d6 100644
--- a/tests/scenarios/Templates/TemplatesWidgetCarouselTest.php
+++ b/tests/scenarios/Templates/TemplatesWidgetCarouselTest.php
@@ -24,9 +24,7 @@
 abstract class TemplatesWidgetCarouselArticlesTestCase
   extends AbstractControllerTestCase {
 
-  protected
-    $_storm_default_to_volatile = true,
-    $_loader_article;
+  protected $_loader_article;
 
   public function setUp() {
     parent::setUp();
@@ -161,8 +159,6 @@ class TemplatesWidgetCarouselLoaderTestTest extends ArticleLoaderGetArticlesByPr
 class TemplatesWidgetCarouselReviewTest
   extends AbstractControllerTestCase {
 
-  protected $_storm_default_to_volatile = true;
-
   public function setUp() {
     parent::setUp();
 
diff --git a/tests/scenarios/Templates/TemplatesWidgetTest.php b/tests/scenarios/Templates/TemplatesWidgetTest.php
index 14b28b3085c2f12c28f4b99b1572e6a977430352..c9a6ec0d93db27a6b1e8da58bc80c1ff0934244d 100644
--- a/tests/scenarios/Templates/TemplatesWidgetTest.php
+++ b/tests/scenarios/Templates/TemplatesWidgetTest.php
@@ -1620,10 +1620,7 @@ class TemplatesWidgetWithTopHighlightLayoutTest extends AbstractTemplatesWidgetW
 
 abstract class TemplatesWidgetRenderChiliWallGridLayoutsTestCase extends AbstractControllerTestCase {
 
-  protected
-    $_storm_default_to_volatile = true,
-    $_layout = '';
-
+  protected $_layout = '';
 
   public function setUp() {
     parent::setUp();
@@ -1637,9 +1634,10 @@ abstract class TemplatesWidgetRenderChiliWallGridLayoutsTestCase extends Abstrac
                   Class_Profil::DIV_MAIN,
                   ['layout' => $this->_layout]);
 
-    $this->fixture('Class_Notice',
-                   ['id' => 456,
-                    'titre_principal' => 'Rahan']);
+    for ($i = 1 ; $i <= 20; $i++)
+    $this->fixture(Class_Notice::class,
+                   ['id' => $i + 450,
+                    'titre_principal' => 'Rahan n°' . $i ]);
 
     $this->dispatch('/opac/index/index/id_profil/72');
   }
@@ -1663,6 +1661,20 @@ class TemplatesWidgetRenderGridLayoutTest extends TemplatesWidgetRenderChiliWall
   public function divClassWallGridMdShouldContainsRahan() {
     $this->assertXPathContentContains('//div[@class="wall_grid_md d-lg-none"]//div[@class="carousel slide multiple_carousel"]', 'Rahan');
   }
+
+
+  /** @test */
+  public function wallGridMdShouldContainsCardDeckThreeColumns() {
+    $this->assertXPathCount('//div[@class="wall_grid_md d-lg-none"]//div[@class="carousel slide multiple_carousel"]//div[@class="carousel-item active"]//div[@class="card-deck"]/div[@class="card_with_overlay card"]',
+                            3);
+  }
+
+
+  /** @test */
+  public function wallGridSmShouldContainsCarouselSlideWithOnlyOneColumn() {
+    $this->assertXPathCount('//div[@class="wall_grid_md d-lg-none"]//div[@class="carousel slide"]//div[@class="carousel-item active"]/div[@class="card_with_overlay card"]',
+                            1);
+  }
 }
 
 
diff --git a/tests/scripts/SelectDbTest.php b/tests/scripts/SelectDbTest.php
index f943fe802a21606d84866c29cc2f324a454e82d0..1c44ba4045cbb91ad9c9825ebe144bfa8105067c 100644
--- a/tests/scripts/SelectDbTest.php
+++ b/tests/scripts/SelectDbTest.php
@@ -26,8 +26,6 @@ class Scripts_SelectDbScriptTest extends PHPUnit_Framework_TestCase {
 
   public function setUp() {
     parent::setUp();
-    Class_Migration_UpdateConfig::setEcho(true);
-    Class_Migration_UpdateDatabaseAfterSelectDb::setEcho(true);
     $this->_db_name_to_restore = Bokeh_Engine::getInstance()->getDbName();
     exec('cd ' . __DIR__ . '/../.. && php -f scripts/select_db.php bokeh_test_db');
   }
@@ -53,6 +51,6 @@ class Scripts_SelectDbScriptTest extends PHPUnit_Framework_TestCase {
 
 
   protected function _restorePreviousDBName() {
-    (new Class_Migration_UpdateConfig)->run($this->_db_name_to_restore);
+    (new Class_Migration_UpdateConfig)->runWithoutEcho($this->_db_name_to_restore);
   }
 }