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.
- package/dist/analyse/niveaux.js +34 -0
- package/dist/cibles/mobile/projet.js +3 -0
- package/dist/cibles/statique/page.js +2 -2
- package/dist/cibles/statique/projet.js +15 -0
- package/dist/cibles/web/css-ecran.js +9 -0
- package/dist/cibles/web/ecran.js +1 -1
- package/dist/cibles/web/interactions.js +14 -0
- package/dist/cibles/web/projet.js +2 -0
- package/dist/navigateur.js +80 -65
- package/dist/parser/parser.js +11 -5
- package/dist/ri/construire.js +3 -0
- package/docs/guide.en.md +20 -7
- package/docs/guide.md +20 -8
- package/package.json +1 -1
package/dist/analyse/niveaux.js
CHANGED
|
@@ -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' }, '
|
|
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"
|
|
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)))
|
package/dist/cibles/web/ecran.js
CHANGED
|
@@ -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"
|
|
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));
|