humalis 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,8 @@ export function niveauxDesSousTitres(instructions, parent = 1) {
14
14
  else
15
15
  dernier = instruction.niveau;
16
16
  }
17
+ else if (instruction.type === 'Bloc' && instruction.genre === 'section')
18
+ niveauxDeLaSection(instruction.corps, dernier);
17
19
  else if (instruction.type === 'Bloc')
18
20
  niveauxDesSousTitres(instruction.corps, dernier);
19
21
  else {
@@ -31,3 +33,35 @@ export function niveauxDesSousTitres(instructions, parent = 1) {
31
33
  };
32
34
  parcourir(instructions);
33
35
  }
36
+ /** Les sous-titres d'un arbre d'instructions, dans l'ordre de lecture. */
37
+ function sousTitres(instructions, vus = []) {
38
+ for (const instruction of instructions) {
39
+ if (instruction.type === 'Element' && instruction.element === 'soustitre')
40
+ vus.push(instruction);
41
+ if (instruction.type === 'Evenement')
42
+ continue;
43
+ for (const cle of ['corps', 'alors', 'sinon']) {
44
+ const suite = instruction[cle];
45
+ if (Array.isArray(suite))
46
+ sousTitres(suite, vus);
47
+ }
48
+ if (instruction.type === 'Selon')
49
+ for (const cas of instruction.cas)
50
+ sousTitres(cas.corps, vus);
51
+ }
52
+ return vus;
53
+ }
54
+ /** Une « section » : son premier sous-titre la titre, les autres descendent d'un niveau au moins. */
55
+ function niveauxDeLaSection(corps, parent) {
56
+ const tous = sousTitres(corps);
57
+ const automatiques = new Set(tous.filter((t) => t.niveau === undefined));
58
+ niveauxDesSousTitres(corps, parent);
59
+ const [premier, ...autres] = tous;
60
+ if (!premier)
61
+ return;
62
+ const sousLaSection = Math.min((premier.niveau ?? 2) + 1, 6);
63
+ for (const titre of autres) {
64
+ if (automatiques.has(titre) && (titre.niveau ?? 2) < sousLaSection)
65
+ titre.niveau = sousLaSection;
66
+ }
67
+ }
@@ -913,6 +913,9 @@ function styles(formulaires = false, blocs = false, theme, codes = []) {
913
913
  `colonne: { gap: 12 },`,
914
914
  `grille: { gap: 16 },`,
915
915
  `menu: { flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', gap: 12 },`,
916
+ // « menu vertical » : une colonne ; « menu repliable » n'a pas de style propre sur le téléphone.
917
+ `menuVertical: { flexDirection: 'column', flexWrap: 'nowrap', alignItems: 'stretch' },`,
918
+ `menuRepliable: {},`,
916
919
  ]
917
920
  : []),
918
921
  ];
@@ -930,7 +930,7 @@ ${prise}${code}}
930
930
  }
931
931
  const balise = BALISES_BLOC[noeud.genre] ?? 'div';
932
932
  // « menu repliable » : le bouton « Menu » en premier enfant (l'écouteur est dans humalis.js).
933
- const bouton = estRepliable(noeud) ? `creer('button', { type: 'button', class: 'humalis-menu-bouton', 'aria-expanded': 'false' }, 'Menu'), ` : '';
933
+ const bouton = estRepliable(noeud) ? `creer('button', { type: 'button', class: 'humalis-menu-bouton', 'aria-expanded': 'false', 'aria-label': 'Menu' }, creer('span', { class: 'humalis-menu-icone', 'aria-hidden': 'true' })), ` : '';
934
934
  this.ecrireGroupe(e, noeud.corps, `creer('${balise}', { class: ${chaine(classesDuBloc(noeud))} }, ${bouton}...`, '),');
935
935
  // « arrive en bas de » ce bloc : la sentinelle observée et le vrai bouton « Voir plus », tant que la condition est vraie.
936
936
  const bas = noeud.id !== undefined ? this.basDesBlocs.get(noeud.id) : undefined;
@@ -1212,7 +1212,7 @@ ${prise}${code}}
1212
1212
  const balise = BALISES_BLOC[noeud.genre] ?? 'div';
1213
1213
  lignes.push(`<${balise}${attributs([['class', classesDuBloc(noeud)]])}>`);
1214
1214
  if (estRepliable(noeud))
1215
- lignes.push('<button type="button" class="humalis-menu-bouton" aria-expanded="false">Menu</button>');
1215
+ lignes.push('<button type="button" class="humalis-menu-bouton" aria-expanded="false" aria-label="Menu"><span class="humalis-menu-icone" aria-hidden="true"></span></button>');
1216
1216
  this.ecrireGroupeHtml(noeud.corps, env, lignes);
1217
1217
  lignes.push(`</${balise}>`);
1218
1218
  const bas = noeud.id !== undefined ? this.basDesBlocs.get(noeud.id) : undefined;
@@ -1153,4 +1153,19 @@ document.addEventListener('click', (evenement) => {
1153
1153
  menu.toggleAttribute('data-ouvert', ouvrir);
1154
1154
  menu.querySelector(':scope > .humalis-menu-bouton')?.setAttribute('aria-expanded', String(ouvrir));
1155
1155
  });
1156
+ // Un clic à côté d'un menu ouvert, ou la touche Échap, le referme.
1157
+ const fermerLesMenus = (garder) => {
1158
+ for (const menu of document.querySelectorAll('.humalis-menu-repliable[data-ouvert]')) {
1159
+ if (garder && menu.contains(garder)) continue;
1160
+ menu.removeAttribute('data-ouvert');
1161
+ menu.querySelector(':scope > .humalis-menu-bouton')?.setAttribute('aria-expanded', 'false');
1162
+ }
1163
+ };
1164
+ document.addEventListener('click', (evenement) => fermerLesMenus(evenement.target instanceof Node ? evenement.target : null), true);
1165
+ document.addEventListener('keydown', (evenement) => {
1166
+ if (evenement.key !== 'Escape') return;
1167
+ const ouvert = document.querySelector('.humalis-menu-repliable[data-ouvert] > .humalis-menu-bouton');
1168
+ fermerLesMenus(null);
1169
+ ouvert?.focus();
1170
+ });
1156
1171
  `;
@@ -53,9 +53,18 @@ export function cssEcran(codes) {
53
53
  regles.push('.humalis-aller-au-contenu { position: absolute; left: 0.5rem; top: -4rem; z-index: 1000; padding: 0.5rem 1rem; border-radius: 0.5rem; background: Canvas; color: CanvasText; }');
54
54
  regles.push('.humalis-aller-au-contenu:focus { top: 0.5rem; }');
55
55
  }
56
+ // « menu vertical » : les liens en colonne, qui ne passent jamais sur une deuxième colonne
57
+ // (un sommaire de hauteur limitée défile au lieu de s'étaler).
58
+ if (codes.some((c) => c.includes('humalis-menu-vertical')))
59
+ regles.push('.humalis-menu-vertical { flex-direction: column; flex-wrap: nowrap; align-items: stretch; }');
56
60
  // « menu repliable » : sur un petit écran, les liens attendent le bouton « Menu ».
57
61
  if (codes.some((c) => c.includes('humalis-menu-repliable'))) {
58
62
  regles.push('.humalis-menu-bouton { display: none; }');
63
+ // L'icône burger : trois traits (le trait du milieu et ses deux ombres), une croix quand le menu est ouvert.
64
+ regles.push('.humalis-menu-bouton { align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; padding: 0; }');
65
+ regles.push('.humalis-menu-icone { position: relative; display: block; width: 1.125rem; height: 2px; border-radius: 2px; background: currentColor; box-shadow: 0 -6px 0 currentColor, 0 6px 0 currentColor; }');
66
+ regles.push('.humalis-menu-bouton[aria-expanded="true"] .humalis-menu-icone { box-shadow: none; transform: rotate(45deg); }');
67
+ regles.push('.humalis-menu-bouton[aria-expanded="true"] .humalis-menu-icone::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: currentColor; transform: rotate(-90deg); }');
59
68
  regles.push('@media (max-width: 720px) { .humalis-menu-repliable { flex-direction: column; align-items: stretch; } .humalis-menu-bouton { display: inline-flex; align-self: flex-start; } .humalis-menu-repliable:not([data-ouvert]) > :not(.humalis-menu-bouton) { display: none; } }');
60
69
  }
61
70
  if (codes.some((c) => /humalis-pied\b/.test(c)))
@@ -157,7 +157,7 @@ export const RENDU_WEB = {
157
157
  const balise = genre === 'section' ? 'section' : genre === 'menu' ? 'nav' : 'div';
158
158
  // « menu repliable » : le bouton « Menu » du petit écran (src/humalis/menu.ts).
159
159
  const repliable = options.some((o) => o.nom === 'menu' && o.valeur === 'repliable');
160
- return `<${balise} className="${toutes.join(' ')}"${ancre !== undefined ? ` id=${JSON.stringify(ancre)}` : ''}${repliable ? ' onClick={menuRepliable}' : ''}>${repliable ? '<button type="button" className="humalis-menu-bouton" aria-expanded="false">Menu</button>' : ''}`;
160
+ return `<${balise} className="${toutes.join(' ')}"${ancre !== undefined ? ` id=${JSON.stringify(ancre)}` : ''}${repliable ? ' onClick={menuRepliable}' : ''}>${repliable ? '<button type="button" className="humalis-menu-bouton" aria-expanded="false" aria-label="Menu"><span className="humalis-menu-icone" aria-hidden="true"></span></button>' : ''}`;
161
161
  },
162
162
  fermer: (genre) => (genre === 'section' ? '</section>' : genre === 'menu' ? '</nav>' : '</div>'),
163
163
  },
@@ -1086,5 +1086,19 @@ export function menuRepliable(evenement: { target: EventTarget; currentTarget: E
1086
1086
  if (!ouvrir && cible.closest('.humalis-menu-bouton, a') === null) return;
1087
1087
  menu.toggleAttribute('data-ouvert', ouvrir);
1088
1088
  bouton?.setAttribute('aria-expanded', String(ouvrir));
1089
+ if (!ouvrir) return;
1090
+ // Ouvert : un clic à côté, ou la touche Échap, le referme (et Échap rend le focus au bouton).
1091
+ const fermer = (e: Event) => {
1092
+ if (e instanceof KeyboardEvent ? e.key !== 'Escape' : menu.contains(e.target as Node)) return;
1093
+ menu.removeAttribute('data-ouvert');
1094
+ bouton?.setAttribute('aria-expanded', 'false');
1095
+ if (e instanceof KeyboardEvent) (bouton as HTMLElement | null)?.focus();
1096
+ document.removeEventListener('click', fermer, true);
1097
+ document.removeEventListener('keydown', fermer);
1098
+ };
1099
+ setTimeout(() => {
1100
+ document.addEventListener('click', fermer, true);
1101
+ document.addEventListener('keydown', fermer);
1102
+ });
1089
1103
  }
1090
1104
  `;
@@ -584,7 +584,9 @@ export function Defilement() {
584
584
  const { pathname, hash } = useLocation();
585
585
  useEffect(() => {
586
586
  if (!hash) {
587
+ // La fenêtre, et la racine si le CSS du site en fait la zone qui défile (en-tête fixe au-dessus).
587
588
  window.scrollTo(0, 0);
589
+ document.getElementById('racine')?.scrollTo(0, 0);
588
590
  return;
589
591
  }
590
592
  const id = decodeURIComponent(hash.slice(1));