Compare commits
21
Commits
600aee78f5
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
101e7124c2 | ||
|
|
329459600b | ||
|
|
51ce65aafe | ||
|
|
85ef9c962d | ||
|
|
07bcb7a6c2 | ||
|
|
15ae4466e7 | ||
|
|
0e3b1c9c2d | ||
|
|
e15236c6eb | ||
|
|
17527bab43 | ||
|
|
855364ee89 | ||
|
|
a242607593 | ||
|
|
38be780911 | ||
|
|
82ee0fd7dd | ||
|
|
0f9241b2eb | ||
|
|
4f7cc17611 | ||
|
|
48e9bf856c | ||
|
|
0637c0f90d | ||
|
|
36c5b1e380 | ||
|
|
2d3842c702 | ||
|
|
c8342bec3e | ||
|
|
c1fd232b25 |
@@ -5,3 +5,4 @@ mode: simple
|
||||
ignored_config_entities:
|
||||
- maillog.settings
|
||||
- migrate_google_sheets.settings
|
||||
- system.performance
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
uuid: 1e484442-5bae-4315-ae20-4d0b024c78b4
|
||||
langcode: fr
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- node.type.evenement
|
||||
module:
|
||||
- language
|
||||
- node
|
||||
id: evenement
|
||||
label: evenement
|
||||
type: 'canonical_entities:node'
|
||||
pattern: '/evenement/[node:title]'
|
||||
selection_criteria:
|
||||
907f1c98-5147-4498-8335-82e3ebaf60ea:
|
||||
id: 'entity_bundle:node'
|
||||
negate: false
|
||||
context_mapping:
|
||||
node: node
|
||||
bundles:
|
||||
evenement: evenement
|
||||
0c9618f8-e01d-4279-9e52-42448b85a480:
|
||||
id: language
|
||||
negate: false
|
||||
context_mapping:
|
||||
language: 'node:langcode:language'
|
||||
langcodes:
|
||||
fr: fr
|
||||
en: en
|
||||
selection_logic: and
|
||||
weight: -10
|
||||
relationships:
|
||||
'node:langcode:language':
|
||||
label: Langue
|
||||
@@ -0,0 +1,34 @@
|
||||
uuid: 509c00ab-d3f2-40b1-824f-8903a4cd9631
|
||||
langcode: fr
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- node.type.projet
|
||||
module:
|
||||
- language
|
||||
- node
|
||||
id: projet
|
||||
label: projet
|
||||
type: 'canonical_entities:node'
|
||||
pattern: '/projet/[node:title]'
|
||||
selection_criteria:
|
||||
d089baeb-3b2d-4b56-8566-eff0fc6312f8:
|
||||
id: 'entity_bundle:node'
|
||||
negate: false
|
||||
context_mapping:
|
||||
node: node
|
||||
bundles:
|
||||
projet: projet
|
||||
9553951e-443c-43df-9f35-49c6f20d54e9:
|
||||
id: language
|
||||
negate: false
|
||||
context_mapping:
|
||||
language: 'node:langcode:language'
|
||||
langcodes:
|
||||
fr: fr
|
||||
en: en
|
||||
selection_logic: and
|
||||
weight: -10
|
||||
relationships:
|
||||
'node:langcode:language':
|
||||
label: Langue
|
||||
@@ -677,8 +677,8 @@ display:
|
||||
value: all
|
||||
title_enable: false
|
||||
title: All
|
||||
title_enable: true
|
||||
title: 'Les projets | {{ arguments.tid }}'
|
||||
title_enable: false
|
||||
title: ''
|
||||
default_argument_type: fixed
|
||||
default_argument_options:
|
||||
argument: ''
|
||||
@@ -783,8 +783,25 @@ display:
|
||||
arguments: false
|
||||
filters: false
|
||||
filter_groups: false
|
||||
header: false
|
||||
css_class: 'views-archives views-archives-projets'
|
||||
display_description: ''
|
||||
header:
|
||||
leshed_helpers_term_links:
|
||||
id: leshed_helpers_term_links
|
||||
table: views
|
||||
field: leshed_helpers_term_links
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
plugin_id: leshed_helpers_term_links
|
||||
empty: true
|
||||
vocabulary: type_de_projet
|
||||
base_path: projets
|
||||
list_title: 'type de projet'
|
||||
show_all: true
|
||||
only_used: true
|
||||
used_bundle: projet
|
||||
exposed_block: true
|
||||
display_extenders: { }
|
||||
path: projets
|
||||
|
||||
@@ -30,6 +30,24 @@ function leshed_helpers_taxonomy_term_update(EntityInterface $entity) {
|
||||
leshed_helpers_ensure_term_alias($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime l'alias d'URL quand un terme d'un vocabulaire géré est supprimé.
|
||||
*/
|
||||
function leshed_helpers_taxonomy_term_delete(EntityInterface $entity) {
|
||||
if (!($entity instanceof TermInterface)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$vid = $entity->bundle();
|
||||
if (!isset(LESHED_HELPERS_VOCABULARIES[$vid])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$internal_path = '/' . LESHED_HELPERS_VOCABULARIES[$vid] . '/' . $entity->id();
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('path_alias');
|
||||
$storage->delete($storage->loadByProperties(['path' => $internal_path]));
|
||||
}
|
||||
|
||||
function leshed_helpers_ensure_term_alias(EntityInterface $entity) {
|
||||
if (!($entity instanceof TermInterface)) {
|
||||
return;
|
||||
@@ -62,3 +80,18 @@ function leshed_helpers_ensure_term_alias(EntityInterface $entity) {
|
||||
'langcode' => $langcode,
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Déclare le plugin d'area "liste de termes en liens" pour les vues.
|
||||
*/
|
||||
function leshed_helpers_views_data() {
|
||||
$data['views']['leshed_helpers_term_links'] = [
|
||||
'title' => t('Liste de termes en liens (Le Shed)'),
|
||||
'help' => t("Liste les termes d'un vocabulaire en liens vers un chemin de base suivi du tid."),
|
||||
'area' => [
|
||||
'id' => 'leshed_helpers_term_links',
|
||||
],
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Plugin d'area Views listant les termes d'un vocabulaire en liens.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\leshed_helpers\Plugin\views\area;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\views\Attribute\ViewsArea;
|
||||
use Drupal\views\Plugin\views\area\AreaPluginBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Affiche les termes d'un vocabulaire en liens vers un chemin filtré par tid.
|
||||
*
|
||||
* Chaque terme pointe vers <base_path>/<tid> ; l'alias géré par
|
||||
* leshed_helpers (ou pathauto) est appliqué automatiquement à la génération
|
||||
* du lien.
|
||||
*/
|
||||
#[ViewsArea('leshed_helpers_term_links')]
|
||||
class TermLinks extends AreaPluginBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Le gestionnaire d'entités.
|
||||
*/
|
||||
protected EntityTypeManagerInterface $entityTypeManager;
|
||||
|
||||
/**
|
||||
* La connexion à la base de données.
|
||||
*/
|
||||
protected Connection $connection;
|
||||
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, Connection $connection) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('database')
|
||||
);
|
||||
}
|
||||
|
||||
protected function defineOptions() {
|
||||
$options = parent::defineOptions();
|
||||
$options['vocabulary'] = ['default' => ''];
|
||||
$options['base_path'] = ['default' => ''];
|
||||
$options['list_title'] = ['default' => ''];
|
||||
$options['show_all'] = ['default' => TRUE];
|
||||
$options['only_used'] = ['default' => FALSE];
|
||||
$options['used_bundle'] = ['default' => ''];
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
|
||||
parent::buildOptionsForm($form, $form_state);
|
||||
|
||||
$vocabularies = [];
|
||||
foreach ($this->entityTypeManager->getStorage('taxonomy_vocabulary')->loadMultiple() as $vocabulary) {
|
||||
$vocabularies[$vocabulary->id()] = $vocabulary->label();
|
||||
}
|
||||
|
||||
$form['vocabulary'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Vocabulaire'),
|
||||
'#options' => $vocabularies,
|
||||
'#default_value' => $this->options['vocabulary'],
|
||||
'#required' => TRUE,
|
||||
];
|
||||
$form['base_path'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Chemin de base'),
|
||||
'#description' => $this->t('Chemin de la vue filtrée, sans slash initial. Exemple : projets.'),
|
||||
'#default_value' => $this->options['base_path'],
|
||||
'#required' => TRUE,
|
||||
];
|
||||
$form['list_title'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Titre de la liste'),
|
||||
'#default_value' => $this->options['list_title'],
|
||||
];
|
||||
$form['show_all'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Ajouter un lien « Tous » vers la page non filtrée'),
|
||||
'#default_value' => $this->options['show_all'],
|
||||
];
|
||||
$form['only_used'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t("N'afficher que les termes utilisés par au moins un contenu publié"),
|
||||
'#default_value' => $this->options['only_used'],
|
||||
];
|
||||
$node_types = [];
|
||||
foreach ($this->entityTypeManager->getStorage('node_type')->loadMultiple() as $node_type) {
|
||||
$node_types[$node_type->id()] = $node_type->label();
|
||||
}
|
||||
$form['used_bundle'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Type de contenu'),
|
||||
'#options' => $node_types,
|
||||
'#empty_option' => $this->t('- Tous types de contenu -'),
|
||||
'#empty_value' => '',
|
||||
'#default_value' => $this->options['used_bundle'],
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name="options[only_used]"]' => ['checked' => TRUE],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render($empty = FALSE) {
|
||||
if (empty($this->options['vocabulary']) || empty($this->options['base_path'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$storage = $this->entityTypeManager->getStorage('taxonomy_term');
|
||||
$tree = $storage->loadTree($this->options['vocabulary']);
|
||||
if (empty($tree)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// tids des termes du vocabulaire, dans l'ordre de l'arborescence.
|
||||
$tids = array_map(static fn($term) => (int) $term->tid, $tree);
|
||||
|
||||
if (!empty($this->options['only_used'])) {
|
||||
// taxonomy_index ne référence que les contenus publiés ; jointure sur
|
||||
// node_field_data pour restreindre au type de contenu voulu. Une seule
|
||||
// requête pour tout le vocabulaire.
|
||||
$query = $this->connection->select('taxonomy_index', 'ti');
|
||||
$query->distinct();
|
||||
$query->fields('ti', ['tid']);
|
||||
$query->condition('ti.tid', $tids, 'IN');
|
||||
$query->join('node_field_data', 'nfd', 'nfd.nid = ti.nid AND nfd.status = 1');
|
||||
if (!empty($this->options['used_bundle'])) {
|
||||
$query->condition('nfd.type', $this->options['used_bundle']);
|
||||
}
|
||||
$used = array_map('intval', $query->execute()->fetchCol());
|
||||
$tids = array_values(array_intersect($tids, $used));
|
||||
}
|
||||
|
||||
$entities = $storage->loadMultiple($tids);
|
||||
|
||||
// tids présents dans les arguments de la vue, pour marquer le lien actif.
|
||||
$current_tids = [];
|
||||
foreach ($this->view->args as $arg) {
|
||||
foreach (explode(',', (string) $arg) as $tid) {
|
||||
$current_tids[] = trim($tid);
|
||||
}
|
||||
}
|
||||
|
||||
$base_path = '/' . trim($this->options['base_path'], '/');
|
||||
$items = [];
|
||||
|
||||
if (!empty($this->options['show_all'])) {
|
||||
$items[] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Tous'),
|
||||
'#url' => Url::fromUserInput($base_path),
|
||||
'#attributes' => $current_tids ? [] : ['class' => ['is-active']],
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($tree as $position) {
|
||||
$term = $entities[(int) $position->tid] ?? NULL;
|
||||
if ($term === NULL || !$term->isPublished()) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $term->label(),
|
||||
'#url' => Url::fromUserInput($base_path . '/' . $term->id()),
|
||||
'#attributes' => in_array((string) $term->id(), $current_tids, TRUE)
|
||||
? ['class' => ['is-active']]
|
||||
: [],
|
||||
];
|
||||
}
|
||||
|
||||
$cache_tags = ['taxonomy_term_list:' . $this->options['vocabulary']];
|
||||
if (!empty($this->options['only_used'])) {
|
||||
// Le filtrage par usage dépend des contenus publiés.
|
||||
$cache_tags[] = 'node_list';
|
||||
}
|
||||
|
||||
return [
|
||||
'#theme' => 'item_list',
|
||||
'#title' => $this->options['list_title'],
|
||||
'#list_type' => 'ul',
|
||||
'#items' => $items,
|
||||
'#attributes' => ['class' => ['leshed-term-links']],
|
||||
'#cache' => [
|
||||
'tags' => $cache_tags,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
+1
-1
File diff suppressed because one or more lines are too long
+6
-6
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
|
||||
"""Génère les favicons « Sh » à partir des glyphes Epilogue variable.
|
||||
Usage : python3 favicon-gen.py <dossier de sortie>"""
|
||||
import sys, os
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.varLib.instancer import instantiateVariableFont
|
||||
from fontTools.pens.svgPathPen import SVGPathPen
|
||||
from fontTools.pens.boundsPen import BoundsPen
|
||||
F = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../fonts/epilogue/")
|
||||
UP, IT = "Epilogue-VariableFont_wght.ttf", "Epilogue-Italic-VariableFont_wght.ttf"
|
||||
|
||||
def glyph(font, ch, w):
|
||||
f = instantiateVariableFont(TTFont(F + font), {"wght": w})
|
||||
gs = f.getGlyphSet(); name = f.getBestCmap()[ord(ch)]
|
||||
p = SVGPathPen(gs); gs[name].draw(p)
|
||||
b = BoundsPen(gs); gs[name].draw(b)
|
||||
return p.getCommands(), b.bounds
|
||||
|
||||
def make(out, letters, gap=40):
|
||||
parts, x = [], None
|
||||
for font, ch, w in letters:
|
||||
d, b = glyph(font, ch, w)
|
||||
dx = -b[0] if x is None else x + gap - b[0]
|
||||
parts.append((d, dx, b)); x = dx + b[2]
|
||||
x0 = 0; W = x
|
||||
y0 = min(b[1] for _, _, b in parts); y1 = max(b[3] for _, _, b in parts); H = y1 - y0
|
||||
S, pad = 1000, 140; sc = (S - 2 * pad) / max(W, H)
|
||||
tx = pad + ((S - 2 * pad) - W * sc) / 2
|
||||
ty = pad + ((S - 2 * pad) - H * sc) / 2 + y1 * sc
|
||||
paths = "".join(f'<path transform="translate({dx:.1f} 0)" d="{d}"/>' for d, dx, _ in parts)
|
||||
open(out, "w").write(
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {S} {S}">'
|
||||
f'<rect width="{S}" height="{S}" rx="180" fill="#fff"/>'
|
||||
f'<g fill="#000" transform="translate({tx:.1f} {ty:.1f}) scale({sc:.4f} {-sc:.4f})">{paths}</g></svg>')
|
||||
|
||||
o = sys.argv[1]
|
||||
# S italique très gras, h droit fin (contraste du logo « Le Shed »).
|
||||
make(o + "/favicon.svg", [(IT, "S", 900), (UP, "h", 100)]) # 180 px et plus
|
||||
make(o + "/favicon-tab.svg", [(IT, "S", 900), (UP, "h", 300)]) # onglets
|
||||
make(o + "/favicon-small.svg", [(IT, "S", 900), (UP, "h", 400)]) # ico 16/32
|
||||
# variante inverse, pour comparaison
|
||||
make(o + "/alt.svg", [(IT, "S", 100), (UP, "h", 900)])
|
||||
make(o + "/alt-small.svg", [(IT, "S", 400), (UP, "h", 900)])
|
||||
@@ -43,6 +43,15 @@ gsap.registerPlugin(ScrollTrigger);
|
||||
const gsap_mm = gsap.matchMedia();
|
||||
console.log('gsap_mm', gsap_mm);
|
||||
|
||||
// Frontière mobile ↔ tablette : source unique = _breakpoints.scss,
|
||||
// publiée en variable CSS --bp-tablette (début inclusif de la zone
|
||||
// tablette) par _base.scss. Plus de littéral ici — même requête que
|
||||
// $desktop-et-tablette, complément de $mobile (max-width: 767.98px) :
|
||||
// le JS est exclusif du CSS mobile (bug « à 768px pile », review #94).
|
||||
const bpTablette = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--bp-tablette').trim() || '768px';
|
||||
const desktopQuery = `(min-width: ${bpTablette})`;
|
||||
|
||||
// Instance Lenis partagée : initPageFull en a besoin pour resynchroniser la
|
||||
// plage de scroll après avoir gonflé le spacer (voir ensureScrollForTitle).
|
||||
let lenis = null;
|
||||
@@ -61,6 +70,11 @@ gsap.registerPlugin(ScrollTrigger);
|
||||
initFsLightbox();
|
||||
initPageFull();
|
||||
// initVues()
|
||||
|
||||
// titles.js traite les titres hors écran par lots, après la première
|
||||
// image : leurs hauteurs peuvent changer après la création des
|
||||
// ScrollTriggers. Lenis suit seul (ResizeObserver) ; ScrollTrigger non.
|
||||
document.addEventListener('leshed:titles-ready', () => ScrollTrigger.refresh(), { once: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +143,7 @@ gsap.registerPlugin(ScrollTrigger);
|
||||
if (!scroller || !firstColumn || !secondColumn) return;
|
||||
|
||||
|
||||
gsap_mm.add('(min-width: 768px)', () => {
|
||||
gsap_mm.add(desktopQuery, () => {
|
||||
[firstColumn, secondColumn].forEach((column) => {
|
||||
gsap.to(column, {
|
||||
// Distance à parcourir pour que le bas de la colonne arrive en
|
||||
@@ -186,7 +200,7 @@ gsap.registerPlugin(ScrollTrigger);
|
||||
// layoutSync() dimensionne les colonnes AVANT la création des ScrollTriggers.
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
gsap_mm.add('(min-width: 768px)', () => {
|
||||
gsap_mm.add(desktopQuery, () => {
|
||||
|
||||
const viewport = () => scroller.clientHeight;
|
||||
|
||||
@@ -405,5 +419,10 @@ gsap.registerPlugin(ScrollTrigger);
|
||||
init()
|
||||
} // end LeShedTheme()
|
||||
|
||||
LeShedTheme()
|
||||
// Démarrage APRÈS la première image : main.js s'exécute juste après
|
||||
// titles.js (deux defer), et son init (Lenis, GSAP, fslightbox qui injecte
|
||||
// un <style>) forçait restyle + mise en page avant que les titres prêts ne
|
||||
// soient peints (~150 ms mesurés sur /projets). rAF → la frame est
|
||||
// planifiée ; setTimeout → on passe après sa peinture.
|
||||
requestAnimationFrame(() => setTimeout(LeShedTheme, 0))
|
||||
})(Drupal, drupalSettings)
|
||||
|
||||
@@ -2,20 +2,12 @@
|
||||
import Splitting from "splitting";
|
||||
import { hyphenateSync } from "hyphen/fr";
|
||||
|
||||
// La classe `variable-title` (posée dans initTitles) masque l'élément via le CSS
|
||||
// (.variable-title:not(.is-ready)) jusqu'à ce qu'il soit splitté et marqué
|
||||
// `is-ready`. Le CSS ne masque rien que ce fichier ne traite pas : cette liste
|
||||
// peut évoluer librement, aucun contenu ne peut rester invisible par désync.
|
||||
const TITLE_SELECTORS = '\
|
||||
#block-leshed-identitedusite>a, \
|
||||
article.node-type-projet>header>h2,\
|
||||
article.node-type-projet>.cols>.left>h2,\
|
||||
article.node-type-projet.view-mode-full h2.node-title,\
|
||||
article.node-type-evenement>header>h2,\
|
||||
article.node-type-evenement.view-mode-full h2.node-title,\
|
||||
article.node-type-evenement.view-mode-calendrier h2.node-title\
|
||||
';
|
||||
// div.views-archives-evenements > h3 > time\
|
||||
// Titres traités : la classe `variable-title` est posée côté serveur
|
||||
// (leshed_preprocess_node, block--system-branding-block.html.twig), donc
|
||||
// présente dès le premier rendu. Le CSS les masque (.variable-title:not(.is-ready))
|
||||
// jusqu'au split : la typo brute n'apparaît jamais. Même classe ici et dans le
|
||||
// CSS : aucune désynchro possible entre ce qui est masqué et ce qui est traité.
|
||||
const TITLE_SELECTOR = '.variable-title';
|
||||
|
||||
// Police Epilogue = axe variable `wght` (100–900) uniquement ; l'italique est
|
||||
// un fichier séparé (font-style).
|
||||
@@ -30,74 +22,165 @@ const TITLE_STYLE = {
|
||||
italicOnBold: 0.6, // proba d'italique sur un pic
|
||||
italicOnMedium: 0.4, // proba d'italique sur un medium
|
||||
italicOnThin: 0.30, // proba d'italique sur une fine
|
||||
// Pas des graisses tirées. Chaque valeur distincte de 'wght' coûte une
|
||||
// instance de la police variable (mesuré : ~300 ms de blocage au chargement
|
||||
// avec des entiers 100–900 au hasard, ~30 ms avec quelques paliers). Au pas
|
||||
// de 25 : 18 graisses distinctes (100–400 et 800–900), invisible à l’œil.
|
||||
wghtStep: 25,
|
||||
};
|
||||
|
||||
function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
// Marge (en hauteurs de viewport) sous l'écran dans laquelle un titre est
|
||||
// traité tout de suite ; au-delà, il l'est par lots pendant les temps morts.
|
||||
const EAGER_VIEWPORTS = 1.5;
|
||||
// Budget d'un lot différé (ms) : on rend la main avant de dépasser.
|
||||
const IDLE_BUDGET = 8;
|
||||
|
||||
/**
|
||||
* Traite d'abord les titres à l'écran (ou juste dessous) pour qu'ils soient
|
||||
* peints au plus tôt, puis le reste par lots pendant les temps morts.
|
||||
*
|
||||
* Sur une liste longue (/projets : 115 titres, ~1900 lettres), tout traiter
|
||||
* d'un bloc bloquait le thread ~300 ms avant la première image. Les titres
|
||||
* hors écran restent masqués (.variable-title:not(.is-ready)) le temps de
|
||||
* leur lot — personne ne les voit — et le garde-fou CSS de 3 s couvre le
|
||||
* pire cas (requestIdleCallback a un timeout de 1 s).
|
||||
*
|
||||
* Fin du traitement : événement `leshed:titles-ready` sur document (main.js
|
||||
* s'en sert pour recaler ScrollTrigger, les hauteurs ayant pu changer).
|
||||
*/
|
||||
function initTitles() {
|
||||
const titles = document.querySelectorAll(TITLE_SELECTORS);
|
||||
const titles = Array.from(document.querySelectorAll(TITLE_SELECTOR));
|
||||
const limit = window.innerHeight * EAGER_VIEWPORTS;
|
||||
// Lectures seules : une seule mise en page pour tout le tri.
|
||||
const eager = [];
|
||||
const deferred = [];
|
||||
for (const txt of titles) {
|
||||
txt.classList.add("variable-title");
|
||||
const splitted = Splitting({ target: txt, by: 'chars' });
|
||||
for (const word of splitted[0].words) {
|
||||
const chars = Array.from(word.getElementsByClassName('char'));
|
||||
styleWordChars(chars);
|
||||
hyphenateWord(chars);
|
||||
const r = txt.getBoundingClientRect();
|
||||
(r.bottom >= 0 && r.top <= limit ? eager : deferred).push(txt);
|
||||
}
|
||||
|
||||
eager.forEach(processTitle);
|
||||
measureOverflow(eager);
|
||||
processDeferred(deferred, () => {
|
||||
document.dispatchEvent(new CustomEvent('leshed:titles-ready'));
|
||||
});
|
||||
|
||||
// La police Epilogue (variable, font-display: block) arrive APRÈS le
|
||||
// premier rendu. Au rechargement, les images sortent du cache instantanément
|
||||
// et la mesure ci-dessus se fait avec les métriques de la police de
|
||||
// substitution : elle conclut « pas de débordement » et plus rien ne la
|
||||
// relance quand la vraie police charge (symptôme : le titre chevauche le
|
||||
// texte sous l'image). On re-mesure les titres déjà traités une fois les
|
||||
// polices prêtes ; ceux des lots suivants sont mesurés à leur traitement.
|
||||
if (document.fonts && document.fonts.ready) {
|
||||
document.fonts.ready.then(() => {
|
||||
measureOverflow(titles.filter((t) => t.classList.contains('is-ready')));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Split + graisses + césures d'un titre, puis le rend visible. */
|
||||
function processTitle(txt) {
|
||||
const splitted = Splitting({ target: txt, by: 'chars' });
|
||||
for (const word of splitted[0].words) {
|
||||
const chars = Array.from(word.getElementsByClassName('char'));
|
||||
styleWordChars(chars);
|
||||
hyphenateWord(chars);
|
||||
breakAfterHyphens(chars);
|
||||
}
|
||||
txt.classList.add('is-ready');
|
||||
}
|
||||
|
||||
/** Traite `queue` par lots de IDLE_BUDGET ms, puis appelle `done`. */
|
||||
function processDeferred(queue, done) {
|
||||
if (!queue.length) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
const schedule = window.requestIdleCallback
|
||||
? (fn) => window.requestIdleCallback(fn, { timeout: 1000 })
|
||||
: (fn) => setTimeout(fn, 16);
|
||||
const step = () => {
|
||||
const end = performance.now() + IDLE_BUDGET;
|
||||
const batch = [];
|
||||
while (queue.length && performance.now() < end) {
|
||||
const txt = queue.shift();
|
||||
processTitle(txt);
|
||||
batch.push(txt);
|
||||
}
|
||||
measureOverflow(batch);
|
||||
queue.length ? schedule(step) : done();
|
||||
};
|
||||
schedule(step);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mesure le débordement des titres sous leur image et pose data-overflow-lines.
|
||||
*
|
||||
* Si un titre déborde de N lignes sous l'image, l'attribut data-overflow-lines
|
||||
* permet au CSS d'augmenter max-height de N lignes pour absorber le débordement.
|
||||
*
|
||||
* Mesures GROUPÉES : toutes les écritures, puis toutes les lectures, puis les
|
||||
* écritures finales — une seule mise en page pour le lot au lieu d'une par
|
||||
* titre (mesuré : ~65 ms de reflows forcés sur /projets en mode intercalé).
|
||||
*/
|
||||
const awaitingLoad = new WeakSet();
|
||||
|
||||
function measureOverflow(h2s) {
|
||||
const items = [];
|
||||
for (const h2 of h2s) {
|
||||
const article = h2.closest('article.has-image');
|
||||
const img = article && article.querySelector('header .field_images img');
|
||||
if (img) items.push({ h2, img });
|
||||
}
|
||||
if (!items.length) return;
|
||||
|
||||
// 1) Écritures : hauteur totale du titre sans le max-height.
|
||||
for (const { h2 } of items) h2.style.maxHeight = 'none';
|
||||
|
||||
// 2) Lectures (une seule mise en page).
|
||||
for (const item of items) {
|
||||
const { h2, img } = item;
|
||||
item.lineHeight = parseFloat(getComputedStyle(h2).lineHeight);
|
||||
// Hauteur réelle de l'image rendue (width:100%, height:auto).
|
||||
item.imgHeight = img.naturalWidth && img.complete
|
||||
? img.clientWidth * (img.naturalHeight / img.naturalWidth)
|
||||
: img.clientHeight;
|
||||
item.titleHeight = h2.scrollHeight;
|
||||
}
|
||||
|
||||
// 3) Écritures finales.
|
||||
for (const { h2, img, lineHeight, imgHeight, titleHeight } of items) {
|
||||
h2.style.maxHeight = '';
|
||||
// Lignes du titre qui dépassent sous l'image.
|
||||
// 1 ligne est déjà dans le flux (max-height: 1em par défaut).
|
||||
if (lineHeight && imgHeight) {
|
||||
const overflowPx = titleHeight - imgHeight;
|
||||
if (overflowPx > 0) {
|
||||
h2.setAttribute('data-overflow-lines', Math.ceil(overflowPx / lineHeight));
|
||||
}
|
||||
}
|
||||
// Hors écran, une image lazy ne déclenche jamais 'load', mais sa boîte de
|
||||
// layout existe dès le départ (attributs width/height de Drupal →
|
||||
// aspect-ratio) : la mesure ci-dessus vaut. Au 'load' réel, on re-mesure.
|
||||
if ((!img.complete || !img.naturalWidth) && !awaitingLoad.has(img)) {
|
||||
awaitingLoad.add(img);
|
||||
img.addEventListener('load', () => measureOverflow([h2]), { once: true });
|
||||
}
|
||||
txt.classList.add("is-ready");
|
||||
measureOverflow(txt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mesure le débordement du titre sous l'image et pose data-overflow-lines.
|
||||
*
|
||||
* Si le titre déborde de N lignes sous l'image, l'attribut data-overflow-lines
|
||||
* permet au CSS d'augmenter max-height de N lignes pour absorber le débordement.
|
||||
* Graisse au hasard dans [min, max], sur les paliers de TITLE_STYLE.wghtStep
|
||||
* (bornes incluses, tirage uniforme entre paliers).
|
||||
*/
|
||||
function measureOverflow(h2) {
|
||||
const article = h2.closest('article.has-image');
|
||||
if (!article) return;
|
||||
|
||||
const img = article.querySelector('header .field_images img');
|
||||
if (!img) return;
|
||||
|
||||
const measure = () => {
|
||||
const lineHeight = parseFloat(getComputedStyle(h2).lineHeight);
|
||||
if (!lineHeight) return;
|
||||
|
||||
// Hauteur réelle de l'image rendue (width:100%, height:auto).
|
||||
let imgHeight;
|
||||
if (img.naturalWidth && img.complete) {
|
||||
imgHeight = img.clientWidth * (img.naturalHeight / img.naturalWidth);
|
||||
} else {
|
||||
imgHeight = img.clientHeight;
|
||||
}
|
||||
if (!imgHeight) return;
|
||||
|
||||
// scrollHeight = hauteur totale du titre sans le max-height.
|
||||
h2.style.maxHeight = 'none';
|
||||
const titleHeight = h2.scrollHeight;
|
||||
h2.style.maxHeight = '';
|
||||
|
||||
// Lignes du titre qui dépassent sous l'image.
|
||||
// 1 ligne est déjà dans le flux (max-height: 1em par défaut).
|
||||
const overflowPx = titleHeight - imgHeight;
|
||||
if (overflowPx <= 0) return;
|
||||
|
||||
const overflowLines = Math.ceil(overflowPx / lineHeight);
|
||||
if (overflowLines > 0) {
|
||||
h2.setAttribute('data-overflow-lines', overflowLines);
|
||||
}
|
||||
};
|
||||
|
||||
if (img.complete && img.naturalWidth) {
|
||||
measure();
|
||||
} else {
|
||||
img.addEventListener('load', measure, { once: true });
|
||||
}
|
||||
function randomWght({ min, max }) {
|
||||
const step = TITLE_STYLE.wghtStep;
|
||||
return min + step * randomInt(0, Math.floor((max - min) / step) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,7 +237,7 @@ function styleWordChars(chars) {
|
||||
}
|
||||
return {
|
||||
char,
|
||||
wght: randomInt(range.min, range.max + 1),
|
||||
wght: randomWght(range),
|
||||
ital: Math.random() < pItal,
|
||||
};
|
||||
});
|
||||
@@ -222,4 +305,22 @@ function hyphenateWord(chars) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Autorise la coupure après un trait d'union (« text-to-speech »).
|
||||
*
|
||||
* Le « - » est isolé dans son span .char : l'opportunité de coupure qu'il
|
||||
* porte normalement ne passe pas dans le flux entre les spans, et le mot
|
||||
* entier ne coupe jamais. Comme pour les césures, on pose un point de coupure
|
||||
* entre les spans : un <wbr>. (Un espace sans chasse U+200B à la place est
|
||||
* ignoré par Firefox après un « - » : mesuré sur /projets, aucune coupure.)
|
||||
*/
|
||||
function breakAfterHyphens(chars) {
|
||||
for (let i = 0; i < chars.length - 1; i++) {
|
||||
const c = chars[i];
|
||||
if (/^[-‐]$/.test(c.textContent) && c.parentNode) {
|
||||
c.parentNode.insertBefore(document.createElement('wbr'), c.nextSibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initTitles();
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// ____ ____ _
|
||||
// | __ )| _ \ __ _ __ _ ___ _ __ _ _| |__
|
||||
// | _ \| |_) / _` / _` |/ _ \ '__| | | | '_ \| | | |
|
||||
// | |_) | _ < (_| | (_| | __/ | | |_| | |_) | |_| |
|
||||
// |____/|_| \_\__,_|\__, |\___|_| \__,_|_.__/ \__, |
|
||||
// |___/ |___/
|
||||
//
|
||||
// Aucun CSS émis. LA seule maison des seuils de largeur du thème.
|
||||
//
|
||||
// ─── Modèle ──────────────────────────────────────────────────────────────────
|
||||
// Thème desktop-first. Chaque valeur de $bp est le DÉBUT d'une zone (sa
|
||||
// première largeur, inclusive). Une zone finit à « début suivant − 0.02px »
|
||||
// ($fin) : syntaxe min-/max-width classique, compatible partout (la syntaxe
|
||||
// d'intervalle « width < X » exige Safari ≥ 16.4). Le 0.02px (convention
|
||||
// Bootstrap) réduit le trou entre deux zones aux largeurs fractionnaires
|
||||
// (zoom, DPR non entier) à l'intervalle ]767.98, 768[ — en pratique jamais.
|
||||
//
|
||||
// narrow < 432 ≤ mobile < 768 ≤ tablette < 1024 ≤ desktop
|
||||
//
|
||||
// 1. Zones imbriquées vers le bas — « cette zone et tout ce qui est
|
||||
// plus étroit » :
|
||||
// @media #{$tablette}{ ... } < 1024 (contient mobile)
|
||||
// @media #{$mobile}{ ... } < 768 (contient narrow)
|
||||
// @media #{$narrow}{ ... } < 432
|
||||
//
|
||||
// 2. Le grand format — desktop + tablette ensemble (≥ 768) :
|
||||
// @media #{$desktop-et-tablette}{ ... }
|
||||
// C'est le complément de $mobile (aucune largeur ne matche les deux) :
|
||||
// pour les composants écrits en deux blocs explicites (grand format /
|
||||
// mobile).
|
||||
//
|
||||
// 3. Formats seuls — exactement une zone, ni plus ni moins :
|
||||
// @media #{$seulement-desktop}{ ... } ≥ 1024
|
||||
// @media #{$seulement-tablette}{ ... } 768 – 1023
|
||||
// @media #{$seulement-mobile}{ ... } 432 – 767
|
||||
// (seulement-narrow ≡ $narrow : rien en dessous.)
|
||||
//
|
||||
// ─── Disciplines ────────────────────────────────────────────────────────────
|
||||
// 1. Jamais de nombre brut égal à un seuil : interpoler une requête nommée.
|
||||
// 2. Une valeur découverte pendant le pass responsive s'écrit brute avec un
|
||||
// commentaire et se consolide ICI quand 2+ composants convergent dessus.
|
||||
// 3. Le JS consomme ces seuils par les variables CSS --bp-* (posées dans
|
||||
// _base.scss) : plus jamais de littéral dans main.js. Le bug « à 768px
|
||||
// pile » de la review (#94) venait d'un chevauchement CSS/JS — les zones
|
||||
// sont exclusives par construction, le JS doit l'être aussi.
|
||||
// 4. Un futur leshed.breakpoints.yml (images responsives) doit reprendre
|
||||
// exactement ces valeurs.
|
||||
// 5. L'ancrage du layout (1112px) n'est pas un seuil : il est calculé dans
|
||||
// _variables.scss ($layout-anchor).
|
||||
|
||||
@use "sass:map";
|
||||
|
||||
$bp: (
|
||||
// Début de la zone mobile ; en dessous : narrow (accueil) — découverte
|
||||
// à re-valider au pass 1 (432 = l'iPhone Pro Max à 430 reste narrow).
|
||||
mobile: 432px,
|
||||
// Début de la zone tablette = fin du mobile. 768 pile est tablette :
|
||||
// c'est la frontière CSS/JS du bug #94.
|
||||
tablette: 768px,
|
||||
// Début de la zone desktop.
|
||||
desktop: 1024px,
|
||||
);
|
||||
|
||||
// Dernière largeur d'une zone : juste avant le début de la suivante.
|
||||
@function fin($zone-suivante) {
|
||||
@return map.get($bp, $zone-suivante) - 0.02px;
|
||||
}
|
||||
|
||||
// ─── Zones imbriquées vers le bas ────────────────────────────────────────────
|
||||
// Dérivées de $bp : changer un seuil recalcule tout (CSS, --bp-*, JS).
|
||||
$tablette: "(max-width: #{fin(desktop)})";
|
||||
$mobile: "(max-width: #{fin(tablette)})";
|
||||
$narrow: "(max-width: #{fin(mobile)})";
|
||||
|
||||
// Le grand format : desktop + tablette — complément de $mobile.
|
||||
$desktop-et-tablette: "(min-width: #{map.get($bp, tablette)})";
|
||||
|
||||
// ─── Formats seuls ───────────────────────────────────────────────────────────
|
||||
$seulement-desktop: "(min-width: #{map.get($bp, desktop)})";
|
||||
$seulement-tablette: "(min-width: #{map.get($bp, tablette)}) and (max-width: #{fin(desktop)})";
|
||||
$seulement-mobile: "(min-width: #{map.get($bp, mobile)}) and (max-width: #{fin(tablette)})";
|
||||
@@ -2,41 +2,30 @@
|
||||
@use "../abstracts/variables" as *;
|
||||
@use "fluid" as *;
|
||||
@use "../abstracts/event-date-time" as *;
|
||||
@use "../abstracts/breakpoints" as *;
|
||||
|
||||
@mixin event_calendar(){
|
||||
// @include evenementListe();
|
||||
border-top: 1px solid #ccc;
|
||||
padding: 2em 0 1em 0;
|
||||
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
header{
|
||||
flex:0 1 auto;
|
||||
// width: 5em;
|
||||
padding:0 1em 0 0;
|
||||
|
||||
.field_dates{
|
||||
@include event-date-time(5em);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.6em;
|
||||
@include event-date-time(3.5em);
|
||||
span.date-sep{
|
||||
display: block;
|
||||
width: 1em;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
transform-origin: center;
|
||||
transform: rotate(90deg) translateX(0.1em);
|
||||
line-height: 0;
|
||||
// transform-origin: center;
|
||||
transform: translateX(0.1em);
|
||||
// line-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.content{
|
||||
flex:1 1 auto;
|
||||
h2.node-title{
|
||||
font-size: 3em;
|
||||
}
|
||||
@@ -59,21 +48,66 @@
|
||||
}
|
||||
|
||||
section.last-cel{
|
||||
flex:0 1 auto;
|
||||
.field_images{
|
||||
a{
|
||||
display: block;
|
||||
width: 12em;
|
||||
width: 100%;
|
||||
height: 12em;
|
||||
overflow: hidden;
|
||||
img{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit:contain;
|
||||
object-position: top right;
|
||||
object-fit:cover;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$desktop-et-tablette}{
|
||||
&{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
|
||||
header{
|
||||
flex:0 1 auto;
|
||||
// width: 5em;
|
||||
padding:0 1em 0 0;
|
||||
|
||||
.field_dates{
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.6em;
|
||||
span.date-sep{
|
||||
transform-origin: center;
|
||||
transform: rotate(90deg) translateX(0.1em);
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
section.content{
|
||||
flex:1 1 auto;
|
||||
}
|
||||
|
||||
section.last-cel{
|
||||
flex:0 1 auto;
|
||||
.field_images{
|
||||
a{
|
||||
width: 12em;
|
||||
height: 12em;
|
||||
img{
|
||||
object-fit:contain;
|
||||
object-position: top right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@use "sass:math";
|
||||
@use "fluid" as *;
|
||||
|
||||
@mixin event-date-time($ref: 10em){
|
||||
@mixin event-date-time($ref: 7em){
|
||||
display: flex;
|
||||
position: relative;
|
||||
// Métriques propres aux dates : elles dépendaient avant du bloc de masquage
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
// → la taille vaut 5rem tant que le viewport tient le layout complet
|
||||
// (contenu + padding latéral), suit la largeur du viewport en dessous,
|
||||
// plancher $min.
|
||||
// Le point de passage est dérivé de $layout-anchor : recalage
|
||||
// automatique si le plafond du layout ou son padding changent.
|
||||
// Le point de passage est dérivé de $layout-anchor (_variables.scss) :
|
||||
// recalage automatique si le plafond du layout ou son padding changent.
|
||||
// Les bornes sont émises en rem : elles suivent la taille de police
|
||||
// choisie par l'utilisateur (identiques au px près avec <html> à 16px).
|
||||
|
||||
@use "sass:math";
|
||||
@use "variables" as *;
|
||||
@@ -30,21 +32,20 @@ $root: 16px; // taille de police de <html>
|
||||
@error "px() : px, rem ou em attendu, reçu #{$size}";
|
||||
}
|
||||
|
||||
// Largeur d'ancrage des clamps : le viewport auquel le layout est plein —
|
||||
// 1080px de contenu + 2 × 16px de padding = 1112px.
|
||||
// Depuis que $layout_padding et $colsgap sont proportionnels (clamp vw),
|
||||
// le padding vaut exactement 16px à ce point (1.4388vw × 1112). Valeur
|
||||
// constante : px() ne sait pas parser un clamp() pour la dériver.
|
||||
$layout-anchor: 1112px;
|
||||
// Convertit px, rem ou em en rem (même hypothèse de base 16px que px()).
|
||||
@function to-rem($size) {
|
||||
@return math.div(px($size), $root) * 1rem;
|
||||
}
|
||||
|
||||
// La valeur vw qui vaut exactement $size quand le viewport fait $at.
|
||||
// La valeur vw qui vaut exactement $size quand le viewport fait $at
|
||||
// ($layout-anchor par défaut : 1080 de contenu + 2 × padding = 1112px).
|
||||
@function vw-at($size, $at: $layout-anchor) {
|
||||
@return math.div(px($size), $at) * 100 * 1vw;
|
||||
}
|
||||
|
||||
// $size au-dessus de $at, fluide en dessous, plancher $min.
|
||||
@function fluid-clamp($size, $at: $layout-anchor, $min: 0px) {
|
||||
@return clamp(#{px($min)}, #{vw-at($size, $at)}, #{px($size)});
|
||||
@return clamp(#{to-rem($min)}, #{vw-at($size, $at)}, #{to-rem($size)});
|
||||
}
|
||||
|
||||
// Tokens dérivés : tailles de référence passées en fluide. Le plancher est
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
@use "variables" as *;
|
||||
@use "fluid" as *;
|
||||
@use "breakpoints" as *;
|
||||
@use "../abstracts/nodes-on-liste" as *;
|
||||
@use "../abstracts/event-calendrier" as *;
|
||||
|
||||
@@ -179,8 +180,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
// ONLY FOR DESKTOP
|
||||
@media (min-width:769px){
|
||||
// DESKTOP + TABLETTE (≥ 768) — complément exact de $mobile (cf. _breakpoints.scss).
|
||||
@media #{$desktop-et-tablette}{
|
||||
div.cols{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -263,8 +264,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
// ONLY FOR MOBILE
|
||||
@media (max-width:768px){
|
||||
|
||||
// MOBILE (≤ 767)
|
||||
@media #{$mobile}{
|
||||
div.cols{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
@use "sass:math";
|
||||
|
||||
$layout_width: 1080px;
|
||||
// Padding et gap proportionnels : sous l'ancrage du layout (voir _fluid.scss,
|
||||
// $layout-anchor), TOUT rétrécit ensemble (typo fluide, gouttières, colonnes) —
|
||||
// les clamps vw restent exacts à toutes les largeurs. Les vw sont calés pour
|
||||
// valoir exactement les bornes hautes à l'ancrage (1112px) : au-dessus, le
|
||||
// design desktop est identique à l'ancien 1em/2em fixe.
|
||||
$layout_padding: clamp(0.75rem, 1.4388vw, 1rem);
|
||||
$default_fwght: 200;
|
||||
$header_height: 70px;
|
||||
$colsgap: clamp(1.5rem, 2.8777vw, 2rem);
|
||||
|
||||
// Bornes hautes du padding latéral et de la gouttière : leur valeur au-dessus
|
||||
// de l'ancrage du layout (design desktop = l'ancien 1em/2em fixe).
|
||||
$layout_padding_max: 1rem;
|
||||
$colsgap_max: 2rem;
|
||||
|
||||
// Ancrage du layout : le viewport auquel le layout est plein — contenu +
|
||||
// 2 × padding latéral (1080 + 2 × 16 = 1112px). Calculé, pas écrit : changer
|
||||
// $layout_width ou $layout_padding_max recale tous les clamps vw (padding,
|
||||
// gouttière, typo fluide de _fluid.scss). Hypothèse : <html> à 16px.
|
||||
$layout-anchor: $layout_width + 2 * math.div($layout_padding_max, 1rem) * 16px;
|
||||
|
||||
// Padding et gap proportionnels : sous l'ancrage, TOUT rétrécit ensemble
|
||||
// (typo fluide, gouttières, colonnes) — les clamps vw restent exacts à toutes
|
||||
// les largeurs. Le vw vaut exactement la borne haute à l'ancrage.
|
||||
$layout_padding: clamp(0.75rem, math.div(math.div($layout_padding_max, 1rem) * 16px, $layout-anchor) * 100vw, $layout_padding_max);
|
||||
$colsgap: clamp(1.5rem, math.div(math.div($colsgap_max, 1rem) * 16px, $layout-anchor) * 100vw, $colsgap_max);
|
||||
|
||||
@@ -4,7 +4,18 @@
|
||||
// | |_) | (_| \__ \ __/
|
||||
// |____/ \__,_|___/\___|
|
||||
|
||||
@use "sass:map";
|
||||
@use "../abstracts/variables" as *;
|
||||
@use "../abstracts/breakpoints" as *;
|
||||
|
||||
// Seuils publiés en variables CSS : le JS (gsap.matchMedia) les lit au lieu
|
||||
// de littéraux — contrat de valeur unique, voir _breakpoints.scss. Chaque
|
||||
// valeur est le début (inclusif) de sa zone.
|
||||
:root{
|
||||
--bp-mobile: #{map.get($bp, mobile)};
|
||||
--bp-tablette: #{map.get($bp, tablette)};
|
||||
--bp-desktop: #{map.get($bp, desktop)};
|
||||
}
|
||||
|
||||
|
||||
a, a:visited{
|
||||
@@ -19,3 +30,10 @@ body{
|
||||
font-weight: $default_fwght;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
form#user-login-form{
|
||||
input{
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,25 @@ header[role="banner"]{
|
||||
justify-content:space-between;
|
||||
align-items: center;
|
||||
padding: 0.2em 0;
|
||||
|
||||
.header-left{
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.header-center{
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.header-rigth{
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
#block-leshed-identitedusite{
|
||||
a{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items:center;
|
||||
>a:first-child{
|
||||
flex: content ;
|
||||
flex: 0 0 3.5em;
|
||||
// color: ;
|
||||
text-decoration: none;
|
||||
font-size: 2em;
|
||||
@@ -31,6 +46,10 @@ header[role="banner"]{
|
||||
// color: $rose;
|
||||
// }
|
||||
}
|
||||
div.site-slogan{
|
||||
flex: 1 1 auto;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right{
|
||||
@@ -50,11 +69,11 @@ header[role="banner"]{
|
||||
background-color: #000;
|
||||
|
||||
}
|
||||
&:hover{
|
||||
&, &::before, &::after{
|
||||
background-color: green;
|
||||
}
|
||||
}
|
||||
// &:hover{
|
||||
// &, &::before, &::after{
|
||||
// // background-color: green;
|
||||
// }
|
||||
// }
|
||||
&::before, &::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -83,8 +102,8 @@ header[role="banner"]{
|
||||
width:100vw;
|
||||
padding: 0 calc((100vw - $layout_width) / 2) 0 calc((100vw - $layout_width) / 2);
|
||||
|
||||
background-color: rgba(255,255,255,0.3);
|
||||
backdrop-filter: blur(7px);
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
backdrop-filter: blur(9px);
|
||||
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
@@ -92,6 +111,11 @@ header[role="banner"]{
|
||||
&[opened]{
|
||||
height: calc(100vh - $header-height-fluid);
|
||||
}
|
||||
|
||||
>div.inner-wrapper{
|
||||
padding: 0 $layout_padding;
|
||||
}
|
||||
|
||||
nav#block-leshed-navigationprincipale{
|
||||
// width: $layout_width;
|
||||
// margin: 0 auto;
|
||||
@@ -105,8 +129,18 @@ header[role="banner"]{
|
||||
a{
|
||||
font-size: 4em;
|
||||
font-weight: 200;
|
||||
text-transform: lowercase;
|
||||
filter: drop-shadow(0 0 2px #fff) drop-shadow(0 0 5px #fff) drop-shadow(0 0 15px #fff);
|
||||
|
||||
display: inline-block;
|
||||
transform-origin: left;
|
||||
transition: all 0.4s ease-in-out;
|
||||
&:hover{
|
||||
transform: scale(1.1);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,7 +164,10 @@ header[role="banner"]{
|
||||
|
||||
}
|
||||
&.is-active{
|
||||
font-weight: 400;
|
||||
a{
|
||||
font-weight: 400;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ div.layout-container{
|
||||
display: block;
|
||||
position: relative;
|
||||
padding: 0.5em 0;
|
||||
flex: 1 1 $header-height-fluid;
|
||||
flex: 0 1 $header-height-fluid;
|
||||
z-index: 100;
|
||||
// position: fixed;
|
||||
// top:0;
|
||||
|
||||
@@ -41,12 +41,13 @@
|
||||
// }
|
||||
}
|
||||
|
||||
// La classe `variable-title` est posée par assets/js/titles.js au moment où
|
||||
// il prend en charge un titre. Le CSS ne masque donc QUE des éléments que le
|
||||
// JS traite : une désynchronisation de listes de sélecteurs ne peut plus
|
||||
// rendre de contenu invisible (c'est ce qui cachait les dates des teasers
|
||||
// pendant 3 s — le garde-fou). Un titre non traité par le JS n'est jamais
|
||||
// masqué : il reste affiché tel quel.
|
||||
// La classe `variable-title` est posée côté serveur (leshed_preprocess_node,
|
||||
// block--system-branding-block.html.twig) : présente dès le premier rendu,
|
||||
// elle permet de masquer le titre AVANT que titles.js ne s'exécute — pas de
|
||||
// typo brute à l'écran. titles.js sélectionne cette même classe : le CSS ne
|
||||
// masque QUE des éléments que le JS traite (pas de liste de sélecteurs à
|
||||
// tenir synchronisée — c'est ce qui cachait les dates des teasers pendant
|
||||
// 3 s, le garde-fou).
|
||||
|
||||
// Avant le split : même métrique qu'après, pour éviter un saut de mise en page.
|
||||
.variable-title:not(.splitting){
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
@use "../abstracts/variables" as *;
|
||||
@use "../abstracts/nodes-on-liste" as *;
|
||||
@use "../abstracts/breakpoints" as *;
|
||||
|
||||
|
||||
div.layout.layout--twocol-section--50-50.layout--twocol-section{
|
||||
@@ -34,7 +35,7 @@ div.layout.layout--twocol-section--50-50.layout--twocol-section{
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:431px) {
|
||||
@media #{$narrow}{
|
||||
&{
|
||||
display:block;
|
||||
div.layout__region{
|
||||
|
||||
@@ -7,12 +7,46 @@
|
||||
|
||||
@use "../abstracts/variables" as *;
|
||||
@use "../abstracts/nodes-on-liste" as *;
|
||||
@use "../abstracts/breakpoints" as *;
|
||||
|
||||
|
||||
div.views-archives-projets{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
// Le header (liste « type de projet ») vit hors de la grille :
|
||||
>header{
|
||||
padding-bottom: 2em;
|
||||
|
||||
h3{
|
||||
font-size: 1em;
|
||||
margin:0 0 0.5em 0;
|
||||
font-weight: 400;
|
||||
// font-size: ;
|
||||
}
|
||||
ul{
|
||||
padding:0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em;
|
||||
li{
|
||||
list-style: none;
|
||||
line-height: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// les rows sont wrappées dans div.view-content (template
|
||||
// views-view--archives--page-1.html.twig), qui porte la grille.
|
||||
div.view-content{
|
||||
// Design : 4 colonnes ou 2, jamais 3 — grille explicite qui bascule au
|
||||
// seuil mobile. Une dernière rangée incomplète ne s'étire pas.
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: $colsgap;
|
||||
@media #{$mobile}{
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
article.node-type-projet.view-mode-tableau_liste{
|
||||
@@ -26,27 +60,6 @@ div.views-archives-projets{
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ONLY FOR DESKTOP
|
||||
@media (min-width:769px){
|
||||
// gap: 2em;
|
||||
gap: $colsgap;
|
||||
div.views-row{
|
||||
flex: 1 0 calc(25% - $colsgap);
|
||||
width: calc(25% - $colsgap);
|
||||
}
|
||||
}
|
||||
|
||||
// ONLY FOR MOBILE
|
||||
@media (max-width:768px){
|
||||
// gap: 2em;
|
||||
gap: $colsgap;
|
||||
div.views-row{
|
||||
flex: 1 0 calc(50% - $colsgap);
|
||||
width: calc(50% - $colsgap);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
form#views-exposed-form-archives-page-1{
|
||||
div.bef-links{
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><rect width="1000" height="1000" rx="180" fill="#fff"/><g fill="#000" transform="translate(140.0 749.0) scale(0.3240 -0.3240)"><path transform="translate(-42.0 0)" d="M548 -26Q408 -26 293.0 17.0Q178 60 110.0 143.0Q42 226 42 346Q42 393 48.0 426.5Q54 460 63 493H466Q459 468 455.5 445.5Q452 423 452 406Q452 353 488.5 319.0Q525 285 600 285Q655 285 702.5 297.5Q750 310 779.0 337.5Q808 365 808 409Q808 468 730.0 514.5Q652 561 533 607Q451 640 370.0 685.0Q289 730 236.0 798.5Q183 867 183 969Q183 1091 235.0 1189.0Q287 1287 377.5 1356.5Q468 1426 585.0 1463.0Q702 1500 832 1500Q977 1500 1091.0 1456.0Q1205 1412 1270.5 1331.0Q1336 1250 1336 1139Q1336 1099 1329.5 1061.0Q1323 1023 1317 1004H914Q916 1013 920.5 1035.5Q925 1058 925 1080Q925 1128 890.5 1159.0Q856 1190 783 1190Q724 1190 678.0 1174.0Q632 1158 605.5 1126.5Q579 1095 579 1046Q579 992 651.0 952.0Q723 912 837 865Q942 822 1027.0 772.5Q1112 723 1162.0 654.0Q1212 585 1212 481Q1212 318 1122.0 204.5Q1032 91 881.0 32.5Q730 -26 548 -26Z"/><path transform="translate(1163.7 0)" d="M934.9987182617188 0V400.9996337890625Q934.9987182617188 430.99951171875 934.9987182617188 470.832763671875Q934.9987182617188 510.666015625 934.9987182617188 551.1659545898438Q934.9987182617188 591.6658935546875 934.9987182617188 621.9990844726562Q934.9987182617188 785.331787109375 862.6658020019531 872.4978942871094Q790.3328857421875 959.6640014648438 640.0000610351562 959.6640014648438Q539.0004272460938 959.6640014648438 460.83416748046875 917.8309631347656Q382.66790771484375 875.9979248046875 338.1681823730469 806.3316040039062Q293.66845703125 736.665283203125 293.66845703125 652.998779296875L211.6668701171875 676.9993286132812Q211.6668701171875 784.3330688476562 269.3336486816406 875.3332214355469Q327.00042724609375 966.3333740234375 428.6672058105469 1021.6667175292969Q530.333984375 1077.0000610351562 661.0006713867188 1077.0000610351562Q851.000732421875 1077.0000610351562 954.6678161621094 961.8334045410156Q1058.3348999023438 846.666748046875 1058.3348999023438 643.666748046875Q1058.3348999023438 590.3334350585938 1058.3348999023438 522.6666870117188Q1058.3348999023438 454.99993896484375 1058.3348999023438 385.3328857421875V0ZM170.332275390625 0V1563H293.66845703125V0Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -11,6 +11,16 @@ global:
|
||||
assets/dist/main.css: {}
|
||||
# assets/scss/main.css: {}
|
||||
|
||||
# CSS compilée seule. En dev, on l'attache AUX CÔTÉS du JS HMR (voir
|
||||
# leshed_page_attachments_alter) : servie en <link> render-blocking, elle
|
||||
# élimine le flash de contenu sans styles pendant que les modules Vite
|
||||
# chargent (eux injectent ensuite les styles à jour par-dessus — plus tard
|
||||
# dans la cascade, ils gagnent les égalités).
|
||||
base_css:
|
||||
css:
|
||||
theme:
|
||||
assets/dist/main.css: {}
|
||||
|
||||
# Rendu typographique des titres, au plus tôt (voir assets/js/titles.js).
|
||||
titles:
|
||||
header: true
|
||||
|
||||
@@ -13,6 +13,12 @@ function leshed_page_attachments_alter(&$page) {
|
||||
// $page['#attached']['library'][] = 'http://localhost:5173/assets/js/main.js';
|
||||
$page['#attached']['library'][] = 'leshed/vitehmr';
|
||||
$page['#attached']['library'][] = 'leshed/titles_hmr';
|
||||
// CSS compilée en <link> render-blocking : sans elle, la page se peint
|
||||
// sans styles pendant le chargement des modules Vite (qui portent la CSS
|
||||
// en dev). Le build --watch tient assets/dist/main.css frais ; les styles
|
||||
// injectés ensuite par Vite, plus tardifs dans la cascade, prennent le
|
||||
// dessus sur les égalités.
|
||||
$page['#attached']['library'][] = 'leshed/base_css';
|
||||
}else{
|
||||
$page['#attached']['library'][] = 'leshed/global';
|
||||
$page['#attached']['library'][] = 'leshed/titles';
|
||||
@@ -36,6 +42,17 @@ function leshed_page_attachments_alter(&$page) {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// favicon.ico (racine du thème) est servi par Drupal ; on ajoute la version
|
||||
// SVG, prioritaire pour les navigateurs qui la gèrent, et l'icône iOS.
|
||||
$theme_path = base_path() . \Drupal::service('extension.list.theme')->getPath('leshed') . '/';
|
||||
$icons = [
|
||||
'leshed_favicon_svg' => ['rel' => 'icon', 'href' => $theme_path . 'favicon.svg', 'type' => 'image/svg+xml'],
|
||||
'leshed_apple_touch_icon' => ['rel' => 'apple-touch-icon', 'href' => $theme_path . 'apple-touch-icon.png'],
|
||||
];
|
||||
foreach ($icons as $key => $attributes) {
|
||||
$page['#attached']['html_head'][] = [['#tag' => 'link', '#attributes' => $attributes], $key];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Implements hook_preprocess_HOOK() for html.html.twig.
|
||||
@@ -85,7 +102,7 @@ function leshed_preprocess_page_title(&$variables) {
|
||||
$base_url = \Drupal\Core\Url::fromUserInput($base_paths[$route_name]);
|
||||
|
||||
$variables['title'] = [
|
||||
'#markup' => '<a href="' . $base_url->toString() . '">' . \Drupal\Component\Utility\Html::escape($base_title) . '</a> | ' . \Drupal\Component\Utility\Html::escape($term_part),
|
||||
'#markup' => '<a href="' . $base_url->toString() . '">' . \Drupal\Component\Utility\Html::escape(\Drupal\Component\Utility\Html::decodeEntities($base_title)) . '</a> | ' . \Drupal\Component\Utility\Html::escape(\Drupal\Component\Utility\Html::decodeEntities($term_part)),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -130,6 +147,74 @@ function parse_menu_item(&$items, $key){
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess_HOOK() for menu.html.twig.
|
||||
*
|
||||
* Burger (block leshed_navigationprincipale, menu 'main') : le lien actif
|
||||
* passe en typo « variable title ». La classe est posée côté serveur pour
|
||||
* être présente au premier rendu — titles.js la cherche à ce moment-là, et
|
||||
* le CSS masque .variable-title jusqu'au split (pas de flash de typo brute).
|
||||
*
|
||||
* Détection du lien actif : on compare le chemin système courant au chemin
|
||||
* interne de chaque lien (même logique que active-link.js / ActiveLinkResponse
|
||||
* Filter, dont la classe `is-active` n'est dans le HTML que pour les réponses
|
||||
* anonymes en cache). On ne peut PAS utiliser `in_active_trail` : les liens
|
||||
* de menu vers des pages de vues ne matchent jamais — la route courante
|
||||
* expose des paramètres (view_id, display_id, arg_0) absents du lien, donc
|
||||
* loadLinksByRoute() ne le retrouve pas et le trail reste vide (mesuré).
|
||||
*/
|
||||
function leshed_preprocess_menu(&$variables) {
|
||||
if (($variables['menu_name'] ?? '') !== 'main') {
|
||||
return;
|
||||
}
|
||||
// La détection dépend du chemin courant, mais le bloc ne varie par défaut
|
||||
// que par route.menu_active_trails:main — vide sur ce site à cause des
|
||||
// liens views (cf. ci-dessous). Sans ce contexte, le bloc mis en cache sur
|
||||
// une page resservirait sur une autre avec le mauvais lien marqué.
|
||||
$variables['#cache']['contexts'][] = 'url.path';
|
||||
// isFrontPage() lit la config page.front : invalider si elle change.
|
||||
$variables['#cache']['tags'][] = 'config:system.site';
|
||||
$current = \Drupal::service('path.current')->getPath();
|
||||
$is_front = \Drupal::service('path.matcher')->isFrontPage();
|
||||
leshed_menu_variable_title($variables['items'], $current, $is_front);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pose la classe .variable-title sur l'item actif (chemin système courant).
|
||||
*
|
||||
* Chemin interne normalisé avec un « / » initial : la route <front> a un
|
||||
* chemin interne vide. La page d'accueil est configurée sur un chemin
|
||||
* système (page.front), donc sur la page d'accueil on marque l'item
|
||||
* <front> via isFrontPage() plutôt que par le chemin.
|
||||
*
|
||||
* Marque au plus UN item (le premier trouvé) : un lien <front> et un lien
|
||||
* pointant la page d'accueil configurée ne doivent pas être marqués ensemble.
|
||||
*
|
||||
* NB : $item est une copie du tableau, mais $item['url'] est un objet Url
|
||||
* partagé — le setOptions() mute bien celui que rendra le template.
|
||||
*
|
||||
* @param string $current
|
||||
* Chemin système courant (ex. « /projets », « / »).
|
||||
* @param bool $is_front
|
||||
* La page courante est la page d'accueil.
|
||||
*/
|
||||
function leshed_menu_variable_title(array $items, $current, $is_front) {
|
||||
foreach ($items as $item) {
|
||||
if (!empty($item['below']) && leshed_menu_variable_title($item['below'], $current, $is_front)) {
|
||||
return TRUE;
|
||||
}
|
||||
$url = $item['url'];
|
||||
if ($url->isRouted()
|
||||
&& (($is_front && $url->getRouteName() === '<front>') || '/' . $url->getInternalPath() === $current)) {
|
||||
$options = $url->getOptions();
|
||||
$options['attributes']['class'][] = 'variable-title';
|
||||
$url->setOptions($options);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess_HOOK() for node.html.twig.
|
||||
*/
|
||||
@@ -144,6 +229,19 @@ function leshed_preprocess_node(&$variables) {
|
||||
|
||||
$variables["attributes"]['class'][] = "view-mode-{$variables['view_mode']}";
|
||||
|
||||
// Titres typographiques (assets/js/titles.js) : la classe est posée ICI,
|
||||
// dans le HTML servi, et non par le JS. Le CSS peut ainsi masquer le titre
|
||||
// dès le premier rendu (.variable-title:not(.is-ready)) au lieu de laisser
|
||||
// voir la typo brute le temps que le module s'exécute ; et le JS
|
||||
// sélectionne cette même classe : une seule liste, aucune désynchro.
|
||||
$variable_titles = [
|
||||
'projet' => ['teaser', 'tableau_liste', 'lie', 'full'],
|
||||
'evenement' => ['teaser', 'tableau_liste', 'calendrier', 'full'],
|
||||
];
|
||||
if (in_array($variables['view_mode'], $variable_titles[$node_type] ?? [], TRUE)) {
|
||||
$variables['title_attributes']['class'][] = 'variable-title';
|
||||
}
|
||||
|
||||
if ($node->hasField('field_images')) {
|
||||
/** @var \Drupal\file\Plugin\\Field\FieldType\FileFieldItemList $field_images */
|
||||
$field_images = $node->get('field_images');
|
||||
@@ -151,6 +249,16 @@ function leshed_preprocess_node(&$variables) {
|
||||
$variables["attributes"]['class'][] = "has-image";
|
||||
}
|
||||
}
|
||||
|
||||
// Tableau liste (projet) : les « Artiste(s) invité(s) » ne s'affichent que
|
||||
// si la liste est courte (3 valeurs ou moins) — au-delà elle alourdit les
|
||||
// listes sans rien apporter à la lecture.
|
||||
if ($node_type === 'projet' && $variables['view_mode'] === 'tableau_liste') {
|
||||
$artistes = $node->get('field_artiste_s_invite_s');
|
||||
if ($artistes && count($artistes) > 3) {
|
||||
unset($variables['content']['field_artiste_s_invite_s']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends "block.html.twig" %}
|
||||
{#
|
||||
/**
|
||||
* @file
|
||||
* Bloc identité du site. Surcharge du gabarit core uniquement pour poser
|
||||
* `variable-title` sur le nom du site, dans le HTML servi (voir
|
||||
* leshed_preprocess_node() et assets/js/titles.js).
|
||||
*/
|
||||
#}
|
||||
{% block content %}
|
||||
{% if site_logo %}
|
||||
<a href="{{ path('<front>') }}" rel="home">
|
||||
<img src="{{ site_logo }}" alt="{{ 'Home'|t }}" fetchpriority="high" />
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if site_name %}
|
||||
<a href="{{ path('<front>') }}" rel="home" class="variable-title">{{ site_name }}</a>
|
||||
{% endif %}
|
||||
{% if site_slogan %}
|
||||
<div class="site-slogan">{{ site_slogan }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -17,6 +17,9 @@
|
||||
{% if content %}
|
||||
<div id="burger-btn"></div>
|
||||
<div{{ attributes }}>
|
||||
{{ content }}
|
||||
<div class="inner-wrapper">
|
||||
{{ content }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
{#
|
||||
/**
|
||||
* @file
|
||||
* Theme override for main view template — page Projets de la vue Archives.
|
||||
*
|
||||
* Les rows sont wrappées dans un div.view-content pour que le header
|
||||
* (liste « type de projet ») ne soit pas un enfant direct de la grille.
|
||||
*
|
||||
* Available variables:
|
||||
* - attributes: Remaining HTML attributes for the element.
|
||||
* - css_name: A CSS-safe version of the view name.
|
||||
* - css_class: The user-specified classes names, if any.
|
||||
* - header: The optional header.
|
||||
* - footer: The optional footer.
|
||||
* - rows: The results of the view query, if any.
|
||||
* - empty: The content to display if there are no rows.
|
||||
* - pager: The optional pager next/prev links to display.
|
||||
* - exposed: Exposed widget form/info to display.
|
||||
* - feed_icons: Optional feed icons to display.
|
||||
* - more: An optional link to the next page of results.
|
||||
* - title: Title of the view, only used when displaying in the admin preview.
|
||||
* - title_prefix: Additional output populated by modules, intended to be
|
||||
* displayed in front of the view title.
|
||||
* - title_suffix: Additional output populated by modules, intended to be
|
||||
* displayed after the view title.
|
||||
* - attachment_before: An optional attachment view to be displayed before the
|
||||
* view content.
|
||||
* - attachment_after: An optional attachment view to be displayed after the
|
||||
* view content.
|
||||
* - dom_id: Unique id for every view being printed to give unique class for
|
||||
* Javascript.
|
||||
*
|
||||
* @see template_preprocess_views_view()
|
||||
*/
|
||||
#}
|
||||
{%
|
||||
set classes = [
|
||||
dom_id ? 'js-view-dom-id-' ~ dom_id,
|
||||
]
|
||||
%}
|
||||
<div{{ attributes.addClass(classes) }}>
|
||||
{{ title_prefix }}
|
||||
{{ title }}
|
||||
{{ title_suffix }}
|
||||
|
||||
{% if header %}
|
||||
<header>
|
||||
{{ header }}
|
||||
</header>
|
||||
{% endif %}
|
||||
|
||||
{{ exposed }}
|
||||
{{ attachment_before }}
|
||||
|
||||
{% if rows -%}
|
||||
<div class="view-content">
|
||||
{{ rows }}
|
||||
</div>
|
||||
{% elseif empty -%}
|
||||
{{ empty }}
|
||||
{% endif %}
|
||||
{{ pager }}
|
||||
|
||||
{{ attachment_after }}
|
||||
{{ more }}
|
||||
|
||||
{% if footer %}
|
||||
<footer>
|
||||
{{ footer }}
|
||||
</footer>
|
||||
{% endif %}
|
||||
|
||||
{{ feed_icons }}
|
||||
</div>
|
||||
@@ -5,6 +5,11 @@ import { fileURLToPath, URL } from 'node:url'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
esbuild: {
|
||||
// Marque console.log comme "pure" : esbuild le retire du build minifié
|
||||
// (console.warn/error sont conservés). Sans effet en dev server.
|
||||
pure: ['console.log'],
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
|
||||
Reference in New Issue
Block a user