layout-07-icon-rail-shell

Coque d'application à rail d'icônes

Poser le cadre d'un tableau de bord en ne dépensant AUCUNE couleur pour la navigation : un rail d'icônes étroit du même blanc que l'application — séparé par un filet d'un pixel et jamais par un aplat —, un en-tête à recherche en pilule, et un titre de page qui est le seul très grand corps de l'écran. Tout le budget de couleur reste disponible pour les données.

layoutweb1440×640px16 benchmarks

Télécharger le .zip

Le fragment RÉEL, dans son propre document, sur le sol de sa charte — pas une image. Ce que vous voyez est exactement ce que vous collerez.

Employer quand

Application ou tableau de bord à navigation stable de six à dix entrées, sur fond clair : le rail donne l'accès, l'en-tête donne la recherche et le compte, le titre donne le contexte, et le contenu commence dessous en pleine largeur restante. L'entrée active est marquée par l'inversion, jamais par une teinte.

Éviter quand

La navigation a des libellés (le rail d'icônes seules devient illisible au-delà de huit entrées sans texte), l'application est sombre (le filet d'un pixel disparaît et il faut un vrai aplat de rail), ou la couleur de marque doit être visible en permanence — ce système la réserve aux données, et l'y ramener supprime sa seule règle.

Notes

La règle qui porte ce pattern est une règle de RENONCEMENT : la navigation ne dépense aucune couleur, pas même pour son entrée active, de sorte que tout le budget chromatique reste disponible pour les données. Deux benchmarks la tiennent — le rail a exactement le blanc de l'application (comparaison de fonds EFFECTIFS, donc valable même si le rail est transparent), et le fond de l'entrée active est achromatique. Un rail teinté ou une pastille active colorée passeraient tous les autres contrôles et détruiraient pourtant le système : sur cet écran l'accent vert ne pèse que 2 % de la surface, et il suffit d'une pastille de plus pour qu'il cesse d'être lu comme une donnée. Le filet est mesuré des DEUX côtés (entre 0,5 et 1,6 px) : à zéro le rail flotte sans limite, au-delà de deux pixels il redevient une bordure d'encadrement, c'est-à-dire l'ornement que ce système n'a nulle part. Le fragment livre la coque AVEC son emplacement de contenu vide : ce n'est pas un oubli, c'est le slot. Le benchmark de largeur du contenu (100 % de l'entre-marges) est ce qui garantit que tout ce qu'on y posera sera ferré sur la même marge que le titre — la seule promesse que la coque doit tenir à ce qui vient après elle.

Mots-clés

coqueshellrailsidebarnavigationdashboardtableau-de-bordrechercheheaderentetetitreapplicationicone

Benchmarks — la géométrie, en assertions vérifiables

Rejouables par node bin/check.mjs layout-07-icon-rail-shell : le pattern se prouve, il ne se croit pas.

assertionmesureattendu
rail étroit : ≈ 6,6 % de la largeur, jamais une colonnebox('.vl-shell__rail').w / W0.066 ± 0.02
LE RAIL EST DU MÊME BLANC QUE L'APPLICATION — c'est de la coque, pas un moduleMath.abs(lum(bgOf($('.vl-shell__rail'))) - lum(bgOf($('.vl-shell__main'))))≤ 0.01
… et il s'en détache par un FILET d'un pixel, seule séparation de la coquenum('.vl-shell__rail','borderRightWidth')0.5 → 1.6
le rail court sur toute la hauteurbox('.vl-shell__rail').h / H1 ± 0.01
UNE SEULE entrée active dans le raildocument.querySelectorAll('.vl-shell__rail .is-on').length1 ± 0
l'entrée active est marquée par l'INVERSION, pas par une teinte : son fond est achromatique(() => { const c = parseRgb(cs('.vl-shell__rail .is-on','backgroundColor')); return Math.max(...c) - Math.min(...c); })()≤ 6
… et c'est un disquenum('.vl-shell__rail .is-on','borderTopLeftRadius') / box('.vl-shell__rail .is-on').h≥ 0.5
la recherche est une pilulenum('.vl-shell__search','borderTopLeftRadius') / box('.vl-shell__search').h≥ 0.5
la recherche occupe environ un tiers de la largeur : assez pour être une zone, pas assez pour être le sujetbox('.vl-shell__search').w / W0.22 → 0.4
le bloc de compte est poussé au bord droitbox('.vl-shell__main').right - num('.vl-shell__main','paddingRight') - box('.vl-shell__account').right0 ± 1.5
le titre de page est le PLUS GRAND corps de la coque, et de loinnum('.vl-shell__title','fontSize') / num('.vl-shell__search','fontSize')≥ 3
le titre est ferré sur la marge de contenu, comme tout ce qui suivraMath.abs(box('.vl-shell__title').left - (box('.vl-shell__main').left + num('.vl-shell__main','paddingLeft')))≤ 2
le contenu prend toute la largeur restantebox('.vl-shell__content').w / (box('.vl-shell__main').w - num('.vl-shell__main','paddingLeft') - num('.vl-shell__main','paddingRight'))1 ± 0.01
aucune ombre nulle part dans la coque['.vl-shell__rail', '.vl-shell__main'].filter((s) => cs(s, 'boxShadow') !== 'none').length0 ± 0
contraste de l'invite de recherche ≥ 4,5contrast('.vl-shell__search')≥ 4.5
aucun débordement de la coqueoverflow('.vl-shell')≤ 0

Le code

fragment — patterns/layout-07-icon-rail-shell.html Télécharger
<!-- layout-07-icon-rail-shell — la coque d'un tableau de bord, sans une seule couleur.
     Couleurs via --vl-* uniquement.

     LA RÈGLE EST UN RENONCEMENT : la navigation ne dépense AUCUNE couleur — le rail a
     exactement le blanc de l'application, l'entrée active est marquée par l'inversion, et le
     rail ne se détache que par un filet d'un pixel. Tout le budget chromatique reste
     disponible pour les données, qui n'en consomment que 2 % sur la référence.

     `.vl-shell__content` est VIDE : ce n'est pas un oubli, c'est le slot. Ce qui s'y posera
     sera ferré sur la même marge que le titre — c'est la seule promesse de la coque. -->
<div class="vl-shell">

  <aside class="vl-shell__rail">
    <svg class="vl-shell__logo" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
      <circle cx="12" cy="12" r="3.4"/>
      <path d="M11.2 0h1.6v6h-1.6zM11.2 18h1.6v6h-1.6zM0 11.2h6v1.6H0zM18 11.2h6v1.6h-6z"/>
      <path d="m3.5 2.4 1.1-1.1 4.2 4.2-1.1 1.1zM15.2 14.1l1.1-1.1 4.2 4.2-1.1 1.1zM2.4 20.5l4.2-4.2 1.1 1.1-4.2 4.2zM14.1 8.8l4.2-4.2 1.1 1.1-4.2 4.2z"/>
    </svg>
    <i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"><path d="M4 10.5 12 4l8 6.5V20H4z"/></svg></i>
    <i class="is-on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="4" y="4" width="6.5" height="6.5" rx="1.6"/><rect x="13.5" y="4" width="6.5" height="6.5" rx="1.6"/><rect x="4" y="13.5" width="6.5" height="6.5" rx="1.6"/><rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.6"/></svg></i>
    <i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M6 10a6 6 0 0 1 12 0c0 4 1.5 5.5 1.5 5.5h-15S6 14 6 10Z"/><path d="M10 19a2 2 0 0 0 4 0"/></svg></i>
    <i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="8" r="3.6"/><path d="M5 20c.6-3.6 3.4-5.6 7-5.6s6.4 2 7 5.6"/></svg></i>
    <i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"><path d="M4 8.5A1.5 1.5 0 0 1 5.5 7h3.2l1.8 2.2h8A1.5 1.5 0 0 1 20 10.7v6.8a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 17.5Z"/></svg></i>
    <i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="12" r="8"/><path d="M12 4v8h8"/></svg></i>
    <span class="vl-shell__spacer"></span>
    <i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 0 0-.2-1.6l2-1.5-2-3.4-2.3 1a7 7 0 0 0-2.8-1.6L14 2h-4l-.4 2.9a7 7 0 0 0-2.8 1.6l-2.3-1-2 3.4 2 1.5a7 7 0 0 0 0 3.2l-2 1.5 2 3.4 2.3-1a7 7 0 0 0 2.8 1.6L10 22h4l.4-2.9a7 7 0 0 0 2.8-1.6l2.3 1 2-3.4-2-1.5c.1-.5.2-1 .2-1.6Z"/></svg></i>
  </aside>

  <div class="vl-shell__main">
    <div class="vl-shell__head">
      <div class="vl-shell__search">
        <span class="vl-shell__well" aria-hidden="true">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="11" cy="11" r="6.4"/><path d="m15.8 15.8 3.4 3.4"/></svg>
        </span>
        Search your today analysis
      </div>
      <div class="vl-shell__account">
        <span class="vl-shell__notif">
          <span class="vl-shell__well" aria-hidden="true">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M6 10a6 6 0 0 1 12 0c0 4 1.5 5.5 1.5 5.5h-15S6 14 6 10Z"/><path d="M10 19a2 2 0 0 0 4 0"/></svg>
          </span>
          Notifications
          <span class="vl-shell__count">4</span>
        </span>
        <span class="vl-shell__avatar" aria-hidden="true">
          <svg viewBox="0 0 58 58">
            <rect width="58" height="58" fill="var(--vl-ramp-2)"/>
            <circle cx="29" cy="23" r="10" fill="var(--vl-ramp-4)"/>
            <path d="M9 58c1.6-11 9.6-17 20-17s18.4 6 20 17z" fill="var(--vl-ramp-4)"/>
          </svg>
        </span>
      </div>
    </div>

    <h1 class="vl-shell__title">Your Sales<br>Analysis</h1>

    <!-- le slot : vide à dessein, ferré sur la même marge que le titre -->
    <div class="vl-shell__content"></div>
  </div>

</div>

<style>
.vl-shell {
  width: 1440px; height: 640px;
  box-sizing: border-box;
  display: flex;
  background: var(--vl-app);
  color: var(--vl-ink);
  overflow: hidden;
  font-family: Montserrat, Poppins, Inter, "Helvetica Neue", Arial, sans-serif;
}
/* AUCUN fond propre : le rail hérite du blanc de l'application, et un filet d'un pixel suffit
   à le détacher. Un aplat gris le ferait lire comme un module, alors que c'est de la coque —
   et il dépenserait une valeur que le système réserve aux données. */
.vl-shell__rail {
  flex: 0 0 var(--vl-rail-w, 95px);
  border-right: 1px solid var(--vl-hairline);
  display: flex; flex-direction: column; align-items: center;
  padding: 26px 0 30px; gap: 20px;
}
.vl-shell__logo { width: 34px; height: 34px; margin-bottom: 24px; }
.vl-shell__rail i {
  width: 44px; height: 44px; border-radius: 14px;
  display: flex; align-items: center; justify-content: center;
}
.vl-shell__rail i svg { width: 20px; height: 20px; }
/* L'inversion, et rien d'autre. Une pastille colorée suffirait à faire perdre à l'accent des
   données son statut d'exception. */
.vl-shell__rail i.is-on { background: var(--vl-invert); color: var(--vl-on-invert); border-radius: 50%; }
.vl-shell__spacer { flex: 1 1 auto; }

.vl-shell__main {
  flex: 1 1 auto; min-width: 0;
  padding: 24px 30px 0 29px;
  display: flex; flex-direction: column;
}
.vl-shell__head { height: 69px; display: flex; align-items: center; gap: 16px; }
.vl-shell__search {
  width: 430px; height: 58px; padding: 0 26px 0 6px;
  box-sizing: border-box;
  border-radius: var(--vl-radius-pill, 999px);
  box-shadow: inset 0 0 0 1px var(--vl-hairline);
  display: flex; align-items: center; gap: 14px;
  font-size: 16px; color: var(--vl-ink-soft);
}
.vl-shell__well {
  flex: 0 0 auto;
  width: 46px; height: 46px; border-radius: 50%;
  background: var(--vl-module);
  color: var(--vl-ink);
  display: flex; align-items: center; justify-content: center;
}
.vl-shell__well svg { width: 19px; height: 19px; }
.vl-shell__account { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.vl-shell__notif {
  height: 58px; padding: 0 20px 0 6px;
  box-sizing: border-box;
  border-radius: var(--vl-radius-pill, 999px);
  box-shadow: inset 0 0 0 1px var(--vl-hairline);
  display: flex; align-items: center; gap: 14px;
  font-size: 16px; font-weight: 500;
}
.vl-shell__count {
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--vl-ramp-3); color: var(--vl-on-invert);
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; font-weight: 600;
}
.vl-shell__avatar { width: 58px; height: 58px; border-radius: 50%; overflow: hidden; }
.vl-shell__avatar svg { width: 100%; height: 100%; display: block; }

.vl-shell__title {
  margin: 18px 0 26px;
  font-size: 66px; line-height: 1.06; font-weight: 500; letter-spacing: -0.02em;
}
.vl-shell__content { flex: 1 1 auto; min-height: 0; }
</style>
tokens de la charte — systems/ref-18-lime-sales-dashboard.json Télécharger
:root {
  --vl-app: #FEFEFE;
  --vl-module: #F2F2F2;
  --vl-module-warm: #FBF9EC;
  --vl-invert: #000000;
  --vl-accent: #5FE85C;
  --vl-ink: #0B0B0B;
  --vl-ink-soft: #6B6B6B;
  --vl-on-invert: #FFFFFF;
  --vl-on-invert-soft: rgba(255,255,255,0.66);
  --vl-hairline: #E6E6E6;
  --vl-ramp-1: #E3E3E3;
  --vl-ramp-2: #CCCCCC;
  --vl-ramp-3: #999999;
  --vl-ramp-4: #666666;
  --vl-ramp-5: #000000;
  --vl-radius-module: 29px;
  --vl-radius-tile: 20px;
  --vl-radius-pill: 999px;
  --vl-gutter: 16px;
  --vl-gutter-row: 11px;
  --vl-rail-w: 95px;
  --vl-margin: 30px;
  --vl-header-h: 69px;
}
les métadonnées — patterns/layout-07-icon-rail-shell.json (télécharger)
{
  "name": "Coque d'application à rail d'icônes",
  "family": "layout",
  "ref": "ref-18-lime-sales-dashboard",
  "intent": "Poser le cadre d'un tableau de bord en ne dépensant AUCUNE couleur pour la navigation : un rail d'icônes étroit du même blanc que l'application — séparé par un filet d'un pixel et jamais par un aplat —, un en-tête à recherche en pilule, et un titre de page qui est le seul très grand corps de l'écran. Tout le budget de couleur reste disponible pour les données.",
  "when_to_use": "Application ou tableau de bord à navigation stable de six à dix entrées, sur fond clair : le rail donne l'accès, l'en-tête donne la recherche et le compte, le titre donne le contexte, et le contenu commence dessous en pleine largeur restante. L'entrée active est marquée par l'inversion, jamais par une teinte.",
  "avoid_when": "La navigation a des libellés (le rail d'icônes seules devient illisible au-delà de huit entrées sans texte), l'application est sombre (le filet d'un pixel disparaît et il faut un vrai aplat de rail), ou la couleur de marque doit être visible en permanence — ce système la réserve aux données, et l'y ramener supprime sa seule règle.",
  "media": [
    "web"
  ],
  "tags": [
    "coque",
    "shell",
    "rail",
    "sidebar",
    "navigation",
    "dashboard",
    "tableau-de-bord",
    "recherche",
    "header",
    "entete",
    "titre",
    "application",
    "icone"
  ],
  "vars": [
    {
      "name": "--vl-app",
      "role": "fond de l'application ET du rail — la même valeur, c'est le principe",
      "default": "#FEFEFE"
    },
    {
      "name": "--vl-module",
      "role": "aplat des puits d'icône",
      "default": "#F2F2F2"
    },
    {
      "name": "--vl-hairline",
      "role": "le filet qui détache le rail — la SEULE séparation de la coque",
      "default": "#E6E6E6"
    },
    {
      "name": "--vl-invert",
      "role": "aplat de l'entrée active du rail",
      "default": "#000000"
    },
    {
      "name": "--vl-ink",
      "role": "icônes, titre de page",
      "default": "#0B0B0B"
    },
    {
      "name": "--vl-ink-soft",
      "role": "texte d'invite de la recherche",
      "default": "#6B6B6B"
    },
    {
      "name": "--vl-on-invert",
      "role": "icône de l'entrée active",
      "default": "#FFFFFF"
    },
    {
      "name": "--vl-rail-w",
      "role": "largeur du rail",
      "default": "95px"
    },
    {
      "name": "--vl-radius-pill",
      "role": "rayon de la recherche et des pilules d'en-tête",
      "default": "999px"
    }
  ],
  "slots": [
    "nav",
    "search",
    "account",
    "title",
    "actions",
    "content"
  ],
  "geometry": {
    "root": ".vl-shell",
    "frame": [
      1440,
      640
    ],
    "ratios": {
      "rail_over_width": 0.066,
      "header_over_width": 0.048,
      "search_over_width": 0.299,
      "title_over_width": 0.046,
      "margin_over_width": 0.021
    },
    "type_px": {
      "title": 66,
      "search": 16
    },
    "pad_ratio": {
      "top": 0.017,
      "x": 0.021,
      "bottom": 0
    }
  },
  "benchmarks": [
    {
      "name": "rail étroit : ≈ 6,6 % de la largeur, jamais une colonne",
      "measure": "box('.vl-shell__rail').w / W",
      "expect": 0.066,
      "tol": 0.02
    },
    {
      "name": "LE RAIL EST DU MÊME BLANC QUE L'APPLICATION — c'est de la coque, pas un module",
      "measure": "Math.abs(lum(bgOf($('.vl-shell__rail'))) - lum(bgOf($('.vl-shell__main'))))",
      "max": 0.01
    },
    {
      "name": "… et il s'en détache par un FILET d'un pixel, seule séparation de la coque",
      "measure": "num('.vl-shell__rail','borderRightWidth')",
      "min": 0.5,
      "max": 1.6
    },
    {
      "name": "le rail court sur toute la hauteur",
      "measure": "box('.vl-shell__rail').h / H",
      "expect": 1,
      "tol": 0.01
    },
    {
      "name": "UNE SEULE entrée active dans le rail",
      "measure": "document.querySelectorAll('.vl-shell__rail .is-on').length",
      "expect": 1,
      "tol": 0
    },
    {
      "name": "l'entrée active est marquée par l'INVERSION, pas par une teinte : son fond est achromatique",
      "measure": "(() => { const c = parseRgb(cs('.vl-shell__rail .is-on','backgroundColor')); return Math.max(...c) - Math.min(...c); })()",
      "max": 6
    },
    {
      "name": "… et c'est un disque",
      "measure": "num('.vl-shell__rail .is-on','borderTopLeftRadius') / box('.vl-shell__rail .is-on').h",
      "min": 0.5
    },
    {
      "name": "la recherche est une pilule",
      "measure": "num('.vl-shell__search','borderTopLeftRadius') / box('.vl-shell__search').h",
      "min": 0.5
    },
    {
      "name": "la recherche occupe environ un tiers de la largeur : assez pour être une zone, pas assez pour être le sujet",
      "measure": "box('.vl-shell__search').w / W",
      "min": 0.22,
      "max": 0.4
    },
    {
      "name": "le bloc de compte est poussé au bord droit",
      "measure": "box('.vl-shell__main').right - num('.vl-shell__main','paddingRight') - box('.vl-shell__account').right",
      "expect": 0,
      "tol": 1.5
    },
    {
      "name": "le titre de page est le PLUS GRAND corps de la coque, et de loin",
      "measure": "num('.vl-shell__title','fontSize') / num('.vl-shell__search','fontSize')",
      "min": 3
    },
    {
      "name": "le titre est ferré sur la marge de contenu, comme tout ce qui suivra",
      "measure": "Math.abs(box('.vl-shell__title').left - (box('.vl-shell__main').left + num('.vl-shell__main','paddingLeft')))",
      "max": 2
    },
    {
      "name": "le contenu prend toute la largeur restante",
      "measure": "box('.vl-shell__content').w / (box('.vl-shell__main').w - num('.vl-shell__main','paddingLeft') - num('.vl-shell__main','paddingRight'))",
      "expect": 1,
      "tol": 0.01
    },
    {
      "name": "aucune ombre nulle part dans la coque",
      "measure": "['.vl-shell__rail', '.vl-shell__main'].filter((s) => cs(s, 'boxShadow') !== 'none').length",
      "expect": 0,
      "tol": 0
    },
    {
      "name": "contraste de l'invite de recherche ≥ 4,5",
      "measure": "contrast('.vl-shell__search')",
      "min": 4.5
    },
    {
      "name": "aucun débordement de la coque",
      "measure": "overflow('.vl-shell')",
      "max": 0
    }
  ],
  "notes": "La règle qui porte ce pattern est une règle de RENONCEMENT : la navigation ne dépense aucune couleur, pas même pour son entrée active, de sorte que tout le budget chromatique reste disponible pour les données. Deux benchmarks la tiennent — le rail a exactement le blanc de l'application (comparaison de fonds EFFECTIFS, donc valable même si le rail est transparent), et le fond de l'entrée active est achromatique. Un rail teinté ou une pastille active colorée passeraient tous les autres contrôles et détruiraient pourtant le système : sur cet écran l'accent vert ne pèse que 2 % de la surface, et il suffit d'une pastille de plus pour qu'il cesse d'être lu comme une donnée.\n\nLe filet est mesuré des DEUX côtés (entre 0,5 et 1,6 px) : à zéro le rail flotte sans limite, au-delà de deux pixels il redevient une bordure d'encadrement, c'est-à-dire l'ornement que ce système n'a nulle part.\n\nLe fragment livre la coque AVEC son emplacement de contenu vide : ce n'est pas un oubli, c'est le slot. Le benchmark de largeur du contenu (100 % de l'entre-marges) est ce qui garantit que tout ce qu'on y posera sera ferré sur la même marge que le titre — la seule promesse que la coque doit tenir à ce qui vient après elle.",
  "id": "layout-07-icon-rail-shell"
}

À côté