humalis 1.0.4 → 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/accessibilite.js +13 -5
- package/dist/analyse/analyse.js +43 -3
- package/dist/analyse/langues.js +30 -10
- package/dist/analyse/medias.js +4 -1
- package/dist/analyse/navigation.js +35 -0
- package/dist/analyse/niveaux.js +67 -0
- package/dist/analyse/noms.js +1 -1
- package/dist/analyse/site.js +30 -8
- package/dist/cibles/commun/colorerCode.js +59 -0
- package/dist/cibles/commun/mouvement.js +110 -4
- package/dist/cibles/mobile/projet.js +10 -2
- package/dist/cibles/mobile/rendu.js +6 -3
- package/dist/cibles/statique/page.js +99 -6
- package/dist/cibles/statique/projet.js +46 -0
- package/dist/cibles/statique/valeurs.js +5 -1
- package/dist/cibles/web/css-ecran.js +31 -1
- package/dist/cibles/web/ecran.js +161 -16
- package/dist/cibles/web/interactions.js +64 -3
- package/dist/cibles/web/projet.js +31 -2
- package/dist/cibles/web/theme.js +11 -2
- package/dist/cibles/web/valeurs.js +17 -0
- package/dist/i18n/messages.js +2 -0
- package/dist/lexer/lexer.js +106 -3
- package/dist/navigateur.js +5289 -0
- package/dist/parser/interactions.js +6 -2
- package/dist/parser/langues.js +1 -1
- package/dist/parser/medias.js +6 -1
- package/dist/parser/parser.js +91 -3
- package/dist/parser/theme.js +10 -1
- package/dist/ri/construire.js +27 -1
- package/dist/vocabulaire/table-interactions.js +16 -1
- package/dist/vocabulaire/table.js +4 -4
- package/docs/guide.en.md +183 -2
- package/docs/guide.md +183 -1
- package/llms.txt +14 -1
- package/package.json +4 -2
|
@@ -31,8 +31,20 @@ function classesUtilisees(codes) {
|
|
|
31
31
|
trouvees.add(`${m[1]}-${m[2]}`);
|
|
32
32
|
return trouvees;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Les attributs d'un bloc « code … avec animation frappe » : sa classe, le moment de la frappe et une
|
|
36
|
+
* clé stable (la ligne et une empreinte du code), pour ne pas refaire la frappe quand la page se redessine.
|
|
37
|
+
*/
|
|
38
|
+
export function attributsFrappe(bloc) {
|
|
39
|
+
if (!bloc.frappe)
|
|
40
|
+
return undefined;
|
|
41
|
+
let empreinte = 0;
|
|
42
|
+
for (const lettre of bloc.code)
|
|
43
|
+
empreinte = (empreinte * 31 + lettre.charCodeAt(0)) >>> 0;
|
|
44
|
+
return { classe: 'humalis-animation-frappe', frappe: bloc.frappe, cle: `${bloc.ligne}-${empreinte.toString(36)}` };
|
|
45
|
+
}
|
|
34
46
|
/** Vrai si une page anime un élément « au defilement » (l'observateur doit alors être écrit). */
|
|
35
|
-
export const animeAuDefilement = (codes) => codes.some((c) => c.includes('humalis-animation-defilement'));
|
|
47
|
+
export const animeAuDefilement = (codes) => codes.some((c) => c.includes('humalis-animation-defilement') || c.includes('humalis-animation-frappe'));
|
|
36
48
|
/** Le CSS du mouvement utilisé, rangé dans le bloc « no-preference » ; une chaîne vide s'il n'y en a pas. */
|
|
37
49
|
export function cssMouvement(codes) {
|
|
38
50
|
const utilisees = classesUtilisees(codes);
|
|
@@ -53,6 +65,15 @@ export function cssMouvement(codes) {
|
|
|
53
65
|
regles.push('.humalis-animation-defilement:not([data-humalis-vu]) { animation: none; }');
|
|
54
66
|
regles.push('.humalis-js .humalis-animation-defilement:not([data-humalis-vu]) { opacity: 0; }');
|
|
55
67
|
}
|
|
68
|
+
else if (option === 'animation' && valeur === 'frappe') {
|
|
69
|
+
// « code … avec animation frappe » : le vrai code reste en place mais transparent (lecteurs d'écran,
|
|
70
|
+
// copie, taille du bloc) ; le script pose par-dessus une copie qui s'écrit lettre par lettre.
|
|
71
|
+
regles.push('.humalis-animation-frappe { position: relative; }');
|
|
72
|
+
regles.push('.humalis-js .humalis-animation-frappe:not([data-humalis-tape="fini"]) > code:not(.humalis-frappe-copie), .humalis-js .humalis-animation-frappe:not([data-humalis-tape="fini"]) > code:not(.humalis-frappe-copie) * { color: transparent; }');
|
|
73
|
+
regles.push('.humalis-frappe-copie { position: absolute; inset: 0; padding: inherit; overflow: hidden; pointer-events: none; }');
|
|
74
|
+
regles.push('.humalis-frappe-copie::after { content: ""; display: inline-block; width: 0.55em; height: 1.1em; margin-left: 1px; vertical-align: text-bottom; background: currentColor; animation: humalis-curseur 1s steps(1) infinite; }');
|
|
75
|
+
regles.push('@keyframes humalis-curseur { 50% { opacity: 0; } }');
|
|
76
|
+
}
|
|
56
77
|
else if (option === 'duree' && DUREES[valeur]) {
|
|
57
78
|
regles.push(`${selecteur} { --humalis-duree: ${DUREES[valeur]}; }`);
|
|
58
79
|
}
|
|
@@ -71,6 +92,16 @@ export function cssMouvement(codes) {
|
|
|
71
92
|
regles.push(`${selecteur}:hover, ${selecteur}:focus-within, ${selecteur}:active { transform: scale(1.03); }`);
|
|
72
93
|
}
|
|
73
94
|
}
|
|
95
|
+
// « en cascade » : le bloc reste en place, ses enfants s'animent l'un après l'autre (80 ms d'écart).
|
|
96
|
+
if (utilisees.has('animation-cascade')) {
|
|
97
|
+
regles.push('.humalis-animation-cascade.humalis-animation-cascade { animation: none; }');
|
|
98
|
+
for (const valeur of Object.keys(ENTREES).filter((v) => utilisees.has(`animation-${v}`))) {
|
|
99
|
+
regles.push(`.humalis-animation-cascade.humalis-animation-${valeur} > * { animation: humalis-${valeur} var(--humalis-duree, 300ms) ease-out var(--humalis-delai, 0ms) backwards; }`);
|
|
100
|
+
}
|
|
101
|
+
for (let rang = 2; rang <= 12; rang++)
|
|
102
|
+
regles.push(`.humalis-animation-cascade > :nth-child(${rang}) { --humalis-delai: ${(rang - 1) * 80}ms; }`);
|
|
103
|
+
regles.push('.humalis-animation-cascade:not([data-humalis-vu]) > * { animation: none; }');
|
|
104
|
+
}
|
|
74
105
|
if (regles.length === 0)
|
|
75
106
|
return '';
|
|
76
107
|
return `\n/* Le mouvement : seulement si l'appareil ne demande pas de réduire les animations. */\n${REQUETE_MOUVEMENT} {\n${regles.map((r) => ` ${r}`).join('\n')}\n}\n`;
|
|
@@ -127,10 +158,73 @@ export const OBSERVATEUR_DEFILEMENT = `function observerLeDefilement() {
|
|
|
127
158
|
if (!('IntersectionObserver' in window)) return;
|
|
128
159
|
const reduire = matchMedia('(prefers-reduced-motion: reduce)');
|
|
129
160
|
const montrer = (element) => element.setAttribute('data-humalis-vu', '');
|
|
161
|
+
// « code … avec animation frappe » : une copie du code s'écrit par-dessus le vrai, puis s'efface.
|
|
162
|
+
// Chaque bloc garde où il en est : redessiné en cours de frappe, il reprend au même endroit ;
|
|
163
|
+
// déjà tapé (un retour sur la page), il est montré entier tout de suite.
|
|
164
|
+
const etats = new Map();
|
|
165
|
+
const taper = (pre) => {
|
|
166
|
+
const code = pre.querySelector(':scope > code');
|
|
167
|
+
const cle = pre.getAttribute('data-humalis-frappe-cle');
|
|
168
|
+
const etat = etats.get(cle) ?? { ecrites: 0, fini: false };
|
|
169
|
+
etats.set(cle, etat);
|
|
170
|
+
if (!code || reduire.matches || etat.fini) {
|
|
171
|
+
pre.setAttribute('data-humalis-tape', 'fini');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const copie = code.cloneNode(true);
|
|
175
|
+
copie.className = 'humalis-frappe-copie';
|
|
176
|
+
copie.setAttribute('aria-hidden', 'true');
|
|
177
|
+
const morceaux = [];
|
|
178
|
+
const parcours = document.createTreeWalker(copie, NodeFilter.SHOW_TEXT);
|
|
179
|
+
while (parcours.nextNode()) {
|
|
180
|
+
const noeud = parcours.currentNode;
|
|
181
|
+
morceaux.push([noeud, noeud.data]);
|
|
182
|
+
noeud.data = '';
|
|
183
|
+
}
|
|
184
|
+
pre.append(copie);
|
|
185
|
+
pre.setAttribute('data-humalis-tape', 'en-cours');
|
|
186
|
+
// Les lettres déjà tapées sur le bloc d'avant reviennent d'un coup.
|
|
187
|
+
let deja = etat.ecrites;
|
|
188
|
+
for (const morceau of morceaux) {
|
|
189
|
+
const part = morceau[1].slice(0, deja);
|
|
190
|
+
morceau[0].data = part;
|
|
191
|
+
deja -= part.length;
|
|
192
|
+
}
|
|
193
|
+
// Une lettre toutes les 30 ms au plus, et pas plus de 5 secondes en tout ; une pause en fin de ligne.
|
|
194
|
+
const total = morceaux.reduce((somme, morceau) => somme + morceau[1].length, 0);
|
|
195
|
+
const pas = Math.max(8, Math.min(30, 5000 / Math.max(total, 1)));
|
|
196
|
+
let rang = 0;
|
|
197
|
+
let position = 0;
|
|
198
|
+
for (let reste = etat.ecrites; rang < morceaux.length && reste > 0; ) {
|
|
199
|
+
const pris = Math.min(reste, morceaux[rang][1].length);
|
|
200
|
+
reste -= pris;
|
|
201
|
+
if (pris === morceaux[rang][1].length) rang++;
|
|
202
|
+
else position = pris;
|
|
203
|
+
}
|
|
204
|
+
const suivante = () => {
|
|
205
|
+
if (!copie.isConnected) return;
|
|
206
|
+
while (rang < morceaux.length && position >= morceaux[rang][1].length) {
|
|
207
|
+
rang++;
|
|
208
|
+
position = 0;
|
|
209
|
+
}
|
|
210
|
+
if (rang >= morceaux.length || reduire.matches) {
|
|
211
|
+
etat.fini = true;
|
|
212
|
+
copie.remove();
|
|
213
|
+
pre.setAttribute('data-humalis-tape', 'fini');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const lettre = morceaux[rang][1][position++];
|
|
217
|
+
morceaux[rang][0].data += lettre;
|
|
218
|
+
etat.ecrites++;
|
|
219
|
+
setTimeout(suivante, lettre === '\\n' ? pas * 6 : pas);
|
|
220
|
+
};
|
|
221
|
+
suivante();
|
|
222
|
+
};
|
|
130
223
|
const observateur = new IntersectionObserver((entrees) => {
|
|
131
224
|
for (const entree of entrees) {
|
|
132
225
|
if (!entree.isIntersecting) continue;
|
|
133
|
-
|
|
226
|
+
if (entree.target.hasAttribute('data-humalis-frappe')) taper(entree.target);
|
|
227
|
+
else montrer(entree.target);
|
|
134
228
|
observateur.unobserve(entree.target);
|
|
135
229
|
}
|
|
136
230
|
});
|
|
@@ -139,6 +233,12 @@ export const OBSERVATEUR_DEFILEMENT = `function observerLeDefilement() {
|
|
|
139
233
|
if (reduire.matches) montrer(element);
|
|
140
234
|
else observateur.observe(element);
|
|
141
235
|
}
|
|
236
|
+
for (const pre of document.querySelectorAll('[data-humalis-frappe]:not([data-humalis-tape])')) {
|
|
237
|
+
if (pre.getAttribute('data-humalis-frappe') === 'defilement' && !reduire.matches) {
|
|
238
|
+
pre.setAttribute('data-humalis-tape', 'attente');
|
|
239
|
+
observateur.observe(pre);
|
|
240
|
+
} else taper(pre);
|
|
241
|
+
}
|
|
142
242
|
};
|
|
143
243
|
document.documentElement.classList.add('humalis-js');
|
|
144
244
|
suivre();
|
|
@@ -148,9 +248,15 @@ export const OBSERVATEUR_DEFILEMENT = `function observerLeDefilement() {
|
|
|
148
248
|
}`;
|
|
149
249
|
/** src/humalis/mouvement.ts du web et du desktop : le même observateur, typé. */
|
|
150
250
|
export const MOUVEMENT_WEB = `// Produit par Humalis : les animations « au defilement » se jouent quand l'élément entre à l'écran.
|
|
151
|
-
${OBSERVATEUR_DEFILEMENT.replace('function observerLeDefilement() {', 'export function observerLeDefilement(): void {')
|
|
251
|
+
${OBSERVATEUR_DEFILEMENT.replace('function observerLeDefilement() {', 'export function observerLeDefilement(): void {')
|
|
252
|
+
.replace('(element) =>', '(element: Element) =>')
|
|
253
|
+
.replace('const etats = new Map();', 'const etats = new Map<string | null, { ecrites: number; fini: boolean }>();')
|
|
254
|
+
.replace('(pre) =>', '(pre: Element) =>')
|
|
255
|
+
.replace('code.cloneNode(true);', 'code.cloneNode(true) as HTMLElement;')
|
|
256
|
+
.replace('const morceaux = [];', 'const morceaux: [Text, string][] = [];')
|
|
257
|
+
.replace('const noeud = parcours.currentNode;', 'const noeud = parcours.currentNode as Text;')}
|
|
152
258
|
`;
|
|
153
259
|
/** Le prérendu capture la page après le script : il retire les marques du mouvement (rien de caché sans JavaScript). */
|
|
154
260
|
export const NETTOYAGE_PRERENDU = ` // Le script du mouvement marque la page (humalis-js, data-humalis-vu) : le HTML prérendu ne garde aucune de ces marques.
|
|
155
|
-
transformHtml: (html) => html.replace(/(<html[^>]*class="[^"]*)\\bhumalis-js\\b/, '$1').replace(/ data-humalis-vu=""/g, ''),
|
|
261
|
+
transformHtml: (html) => html.replace(/(<html[^>]*class="[^"]*)\\bhumalis-js\\b/, '$1').replace(/ data-humalis-vu=""/g, '').replace(/ data-humalis-tape="[a-z-]*"/g, '').replace(/<code class="humalis-frappe-copie"[\\s\\S]*?<\\/code>/g, ''),
|
|
156
262
|
`;
|
|
@@ -885,6 +885,9 @@ function styles(formulaires = false, blocs = false, theme, codes = []) {
|
|
|
885
885
|
`titre: { fontSize: 28, fontWeight: '700', color: '${encre}', marginVertical: 12 },`,
|
|
886
886
|
`sousTitre: { fontSize: 22, fontWeight: '700', color: '${encre}', marginVertical: 10 },`,
|
|
887
887
|
`texte: { fontSize: 16, lineHeight: 24, color: '${encre}', marginVertical: 6 },`,
|
|
888
|
+
...(codes.some((c) => c.includes('s.accent')) ? [theme?.couleurs.some((c) => c.nom === 'marque')
|
|
889
|
+
? `accent: { color: '${theme.couleurs.find((c) => c.nom === 'marque')[mode === 'clair' ? 'clair' : 'sombre']}' },`
|
|
890
|
+
: `accent: { fontWeight: '700' },`] : []),
|
|
888
891
|
`bouton: { alignSelf: 'flex-start', paddingVertical: 8, paddingHorizontal: 16, marginVertical: 4, borderWidth: 1, borderColor: '${encre}', borderRadius: 6 },`,
|
|
889
892
|
`texteBouton: { fontSize: 16, color: '${encre}' },`,
|
|
890
893
|
`lien: { alignSelf: 'flex-start', fontSize: 16, color: '${bleu}', marginVertical: 12, textDecorationLine: 'underline' },`,
|
|
@@ -910,6 +913,9 @@ function styles(formulaires = false, blocs = false, theme, codes = []) {
|
|
|
910
913
|
`colonne: { gap: 12 },`,
|
|
911
914
|
`grille: { gap: 16 },`,
|
|
912
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: {},`,
|
|
913
919
|
]
|
|
914
920
|
: []),
|
|
915
921
|
];
|
|
@@ -1046,10 +1052,12 @@ l'écraser.
|
|
|
1046
1052
|
/** « changer theme par sombre » sur le téléphone : le réglage de l'application, pour cette session. */
|
|
1047
1053
|
const THEME_MOBILE = `import { Appearance } from 'react-native';
|
|
1048
1054
|
|
|
1049
|
-
export type Theme = 'clair' | 'sombre' | 'automatique';
|
|
1055
|
+
export type Theme = 'clair' | 'sombre' | 'automatique' | 'bascule';
|
|
1050
1056
|
|
|
1051
1057
|
/** Change le thème de l'application (le choix n'est pas gardé après la fermeture). */
|
|
1052
|
-
export function definirTheme(
|
|
1058
|
+
export function definirTheme(demande: Theme): void {
|
|
1059
|
+
// « basculer theme » : l'inverse du thème affiché.
|
|
1060
|
+
const theme = demande === 'bascule' ? (Appearance.getColorScheme() === 'dark' ? 'clair' : 'sombre') : demande;
|
|
1053
1061
|
Appearance.setColorScheme(theme === 'clair' ? 'light' : theme === 'sombre' ? 'dark' : 'unspecified');
|
|
1054
1062
|
}
|
|
1055
1063
|
`;
|
|
@@ -13,7 +13,7 @@ const SANS_STYLE = new Set(['sur', 'degrade', 'colle', 'animation', 'duree', 'de
|
|
|
13
13
|
* et « effet » sont sans effet sur le téléphone.
|
|
14
14
|
*/
|
|
15
15
|
function ouvrirAnime(options) {
|
|
16
|
-
const animation = options.find((o) => o.nom === 'animation' && o.valeur !== 'defilement');
|
|
16
|
+
const animation = options.find((o) => o.nom === 'animation' && o.valeur !== 'defilement' && o.valeur !== 'cascade');
|
|
17
17
|
if (!animation)
|
|
18
18
|
return '';
|
|
19
19
|
const duree = MILLISECONDES.duree[options.find((o) => o.nom === 'duree')?.valeur ?? 'moyenne'] ?? 300;
|
|
@@ -136,12 +136,15 @@ function elementMobile(el) {
|
|
|
136
136
|
const options = stylesDe(el.options);
|
|
137
137
|
// Le texte posé sur un fond prend la couleur d'encre de ce fond.
|
|
138
138
|
const encres = el.options.filter((o) => o.nom === 'fond').map((o) => camel(`encre_${o.valeur}`));
|
|
139
|
+
const contenu = ['titre', 'soustitre', 'texte'].includes(el.genre)
|
|
140
|
+
? el.contenu.replace(/<span className="humalis-accent">/g, '<Text style={s.accent}>').replace(/<code className="humalis-code-ligne">/g, '<Text style={{ fontFamily: "monospace" }}>').replace(/<\/(span|code)>/g, '</Text>')
|
|
141
|
+
: el.contenu;
|
|
139
142
|
switch (el.genre) {
|
|
140
143
|
case 'titre':
|
|
141
144
|
case 'soustitre':
|
|
142
|
-
return `<Text ${id} accessibilityRole="header" ${style([el.niveau === 1 ? 'titre' : 'sousTitre', ...options, ...encres])}${evenements}>${
|
|
145
|
+
return `<Text ${id} accessibilityRole="header" ${style([el.niveau === 1 ? 'titre' : 'sousTitre', ...options, ...encres])}${evenements}>${contenu}</Text>`;
|
|
143
146
|
case 'texte':
|
|
144
|
-
return `<Text ${id} ${style(['texte', ...options, ...encres])}${evenements}>${
|
|
147
|
+
return `<Text ${id} ${style(['texte', ...options, ...encres])}${evenements}>${contenu}</Text>`;
|
|
145
148
|
case 'bouton': {
|
|
146
149
|
// Le fond, la bordure et la position vont au bouton ; la couleur et la taille, à son texte.
|
|
147
150
|
const duBouton = el.options.filter((o) => ['fond', 'bordure', 'position'].includes(o.nom)).map(nomStyle);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { verificationsDuChamp, messageEnCode, libellesEnLangues } from '../../i18n/messages.js';
|
|
1
|
+
import { verificationsDuChamp, messageEnCode, libellesEnLangues, messageEnLangues } from '../../i18n/messages.js';
|
|
2
2
|
/**
|
|
3
3
|
* Un écran de la représentation intermédiaire → une page du site statique :
|
|
4
4
|
* son contenu de départ, écrit dans le HTML (décision T2), et son script,
|
|
@@ -9,6 +9,8 @@ import { verificationsDuChamp, messageEnCode, libellesEnLangues } from '../../i1
|
|
|
9
9
|
* script doivent donner exactement la même page.
|
|
10
10
|
*/
|
|
11
11
|
import { sousActions } from '../../ri/actions.js';
|
|
12
|
+
import { colorerCodeHumalis } from '../commun/colorerCode.js';
|
|
13
|
+
import { attributsFrappe } from '../commun/mouvement.js';
|
|
12
14
|
import { arbreWidget, arbreComposite, initiales } from './widgets.js';
|
|
13
15
|
import { iconeHtml, etoilesHtml } from './html-elements.js';
|
|
14
16
|
import { appelGraphique, htmlGraphique } from './graphique.js';
|
|
@@ -320,9 +322,13 @@ class GenerateurPage {
|
|
|
320
322
|
this.nomsPris.add(resultat);
|
|
321
323
|
return resultat;
|
|
322
324
|
}
|
|
325
|
+
/** « Copier » dans chaque langue du site, la langue de base (celle du HTML de départ) en premier. */
|
|
326
|
+
messageCopier() {
|
|
327
|
+
return (this.site.langues && messageEnLangues('Copier', this.site.langues.map((l) => l.code))) || [{ code: 'fr', texte: 'Copier' }];
|
|
328
|
+
}
|
|
323
329
|
// ─── La page ────────────────────────────────────────────────────────────
|
|
324
330
|
generer() {
|
|
325
|
-
const { titre, description } = titreEtDescription(this.ecran);
|
|
331
|
+
const { titre, description } = titreEtDescription(this.ecran, this.site.langues);
|
|
326
332
|
// Les fonctions et le dessin d'abord : on y lit les variables partagées
|
|
327
333
|
// que la page utilise vraiment.
|
|
328
334
|
const fonctions = new Ecrivain();
|
|
@@ -600,7 +606,7 @@ ${prise}${code}}
|
|
|
600
606
|
break;
|
|
601
607
|
case 'copie':
|
|
602
608
|
// « copier code » : le résultat est dit à la personne, sauf « sans message ».
|
|
603
|
-
e.ligne(`await copier(${this.enTexte(action.valeur)}${action.message === null ? ', null' : action.message === 'Copié' ? '' : `, ${chaine(action.message)}`});`);
|
|
609
|
+
e.ligne(`await copier(${this.enTexte(action.valeur)}${action.message === null ? ', null' : action.texte ? `, ${x(action.texte)}` : action.message === 'Copié' ? '' : `, ${chaine(action.message)}`});`);
|
|
604
610
|
break;
|
|
605
611
|
case 'partage': {
|
|
606
612
|
// La feuille de partage du système ; sans elle, le lien (ou le texte) est copié.
|
|
@@ -892,13 +898,40 @@ ${prise}${code}}
|
|
|
892
898
|
case 'graphique':
|
|
893
899
|
e.ligne(`${appelGraphique(noeud, (v) => expressionJs(v, this.contexte))},`);
|
|
894
900
|
break;
|
|
901
|
+
case 'codeDocumentation':
|
|
902
|
+
const frappe = attributsFrappe(noeud);
|
|
903
|
+
const marques = frappe ? `, 'data-humalis-frappe': '${frappe.frappe}', 'data-humalis-frappe-cle': '${frappe.cle}'` : '';
|
|
904
|
+
e.ligne(`creer('div', { class: 'humalis-bloc-code-conteneur' }, creer('pre', { class: 'humalis-bloc-code${frappe ? ` ${frappe.classe}` : ''}', 'data-langage': ${chaine(noeud.langage ?? '')}${marques} }, (() => { const code = document.createElement('code'); code.innerHTML = ${chaine(colorerCodeHumalis(noeud.code, noeud.langage))}; return code; })()), creer('button', { type: 'button', onclick: () => copier(${chaine(noeud.code)}) }, ${messageEnCode(this.messageCopier(), chaine)})),`);
|
|
905
|
+
break;
|
|
906
|
+
case 'tableauDocumentation': {
|
|
907
|
+
const cellule = (valeur, balise) => `creer('${balise}', {}, ${this.contenuAccentJs(valeur, 'texte')})`;
|
|
908
|
+
const rang = (valeurs, balise) => `creer('tr', {}, ${valeurs.map((v) => cellule(v, balise)).join(', ')})`;
|
|
909
|
+
e.ligne(`creer('div', { class: 'humalis-tableau-conteneur' }, creer('table', {}, creer('caption', {}, ${this.contenuAccentJs(noeud.legende, 'texte')}), creer('thead', {}, ${rang(noeud.colonnes, 'th')}), creer('tbody', {}, ${noeud.rangees.map((r) => rang(r, 'td')).join(', ')}))),`);
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
895
912
|
case 'bloc': {
|
|
913
|
+
if (noeud.genre === 'liste' || noeud.genre === 'listeNumerotee') {
|
|
914
|
+
const balise = noeud.genre === 'liste' ? 'ul' : 'ol';
|
|
915
|
+
e.ligne(`creer('${balise}', { class: 'humalis-liste' },`);
|
|
916
|
+
e.indenter(() => noeud.corps.forEach((enfant) => {
|
|
917
|
+
if (enfant.type === 'repetition') {
|
|
918
|
+
const debut = `...${expressionJs(enfant.liste, this.contexte)}.flatMap((${nomJs(enfant.variable)}) => `;
|
|
919
|
+
e.bloc(`${debut}[`, () => this.ecrireGroupe(e, enfant.corps, "creer('li', {}, ...", '),'), ']),');
|
|
920
|
+
}
|
|
921
|
+
else
|
|
922
|
+
this.ecrireGroupe(e, [enfant], "creer('li', {}, ...", '),');
|
|
923
|
+
}));
|
|
924
|
+
e.ligne('),');
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
896
927
|
if (COMPOSITES.has(noeud.genre)) {
|
|
897
928
|
this.ecrireEnfantJs(e, arbreComposite(this.aideComposite(noeud, this.environnement())));
|
|
898
929
|
break;
|
|
899
930
|
}
|
|
900
931
|
const balise = BALISES_BLOC[noeud.genre] ?? 'div';
|
|
901
|
-
|
|
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', 'aria-label': 'Menu' }, creer('span', { class: 'humalis-menu-icone', 'aria-hidden': 'true' })), ` : '';
|
|
934
|
+
this.ecrireGroupe(e, noeud.corps, `creer('${balise}', { class: ${chaine(classesDuBloc(noeud))} }, ${bouton}...`, '),');
|
|
902
935
|
// « arrive en bas de » ce bloc : la sentinelle observée et le vrai bouton « Voir plus », tant que la condition est vraie.
|
|
903
936
|
const bas = noeud.id !== undefined ? this.basDesBlocs.get(noeud.id) : undefined;
|
|
904
937
|
if (bas) {
|
|
@@ -963,7 +996,7 @@ ${prise}${code}}
|
|
|
963
996
|
const contenu = (lucideIcone ? `icone(${chaine(lucideIcone)}, 'humalis-icone'), ` : '') +
|
|
964
997
|
(this.textesChanges.has(noeud.id)
|
|
965
998
|
? `textes[${chaine(noeud.id)}] ?? ${contenuJs(noeud.contenu, this.contexte)}`
|
|
966
|
-
:
|
|
999
|
+
: this.contenuAccentJs(noeud.contenu, noeud.genre));
|
|
967
1000
|
switch (noeud.genre) {
|
|
968
1001
|
case 'titre':
|
|
969
1002
|
case 'soustitre':
|
|
@@ -1137,13 +1170,49 @@ ${prise}${code}}
|
|
|
1137
1170
|
case 'graphique':
|
|
1138
1171
|
lignes.push(htmlGraphique(noeud, (v) => this.initial(v, env), (v) => this.texteInitial(v, env)));
|
|
1139
1172
|
break;
|
|
1173
|
+
case 'codeDocumentation':
|
|
1174
|
+
const frappe = attributsFrappe(noeud);
|
|
1175
|
+
const marques = frappe ? ` data-humalis-frappe="${frappe.frappe}" data-humalis-frappe-cle="${frappe.cle}"` : '';
|
|
1176
|
+
lignes.push(`<div class="humalis-bloc-code-conteneur"><pre class="humalis-bloc-code${frappe ? ` ${frappe.classe}` : ''}" data-langage="${echapperTexte(noeud.langage ?? '')}"${marques}><code>${colorerCodeHumalis(noeud.code, noeud.langage)}</code></pre><button type="button">${echapperTexte(this.messageCopier()[0].texte)}</button></div>`);
|
|
1177
|
+
break;
|
|
1178
|
+
case 'tableauDocumentation': {
|
|
1179
|
+
const cellule = (v, balise) => `<${balise}>${this.contenuAccentHtml(v, 'texte', env)}</${balise}>`;
|
|
1180
|
+
lignes.push('<div class="humalis-tableau-conteneur"><table>');
|
|
1181
|
+
lignes.push(`<caption>${this.contenuAccentHtml(noeud.legende, 'texte', env)}</caption><thead><tr>${noeud.colonnes.map((v) => cellule(v, 'th')).join('')}</tr></thead><tbody>`);
|
|
1182
|
+
for (const rangee of noeud.rangees)
|
|
1183
|
+
lignes.push(`<tr>${rangee.map((v) => cellule(v, 'td')).join('')}</tr>`);
|
|
1184
|
+
lignes.push('</tbody></table></div>');
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1140
1187
|
case 'bloc': {
|
|
1188
|
+
if (noeud.genre === 'liste' || noeud.genre === 'listeNumerotee') {
|
|
1189
|
+
const balise = noeud.genre === 'liste' ? 'ul' : 'ol';
|
|
1190
|
+
lignes.push(`<${balise} class="humalis-liste">`);
|
|
1191
|
+
for (const enfant of noeud.corps) {
|
|
1192
|
+
if (enfant.type === 'repetition') {
|
|
1193
|
+
const valeurs = evaluer(enfant.liste, env);
|
|
1194
|
+
for (const valeur of Array.isArray(valeurs) ? valeurs : []) {
|
|
1195
|
+
lignes.push('<li>');
|
|
1196
|
+
this.ecrireGroupeHtml(enfant.corps, env.enfant().definir(enfant.variable, valeur), lignes);
|
|
1197
|
+
lignes.push('</li>');
|
|
1198
|
+
}
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
lignes.push('<li>');
|
|
1202
|
+
this.ecrireGroupeHtml([enfant], env, lignes);
|
|
1203
|
+
lignes.push('</li>');
|
|
1204
|
+
}
|
|
1205
|
+
lignes.push(`</${balise}>`);
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1141
1208
|
if (COMPOSITES.has(noeud.genre)) {
|
|
1142
1209
|
this.ecrireEnfantHtml(arbreComposite(this.aideComposite(noeud, env)), env, lignes);
|
|
1143
1210
|
break;
|
|
1144
1211
|
}
|
|
1145
1212
|
const balise = BALISES_BLOC[noeud.genre] ?? 'div';
|
|
1146
1213
|
lignes.push(`<${balise}${attributs([['class', classesDuBloc(noeud)]])}>`);
|
|
1214
|
+
if (estRepliable(noeud))
|
|
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>');
|
|
1147
1216
|
this.ecrireGroupeHtml(noeud.corps, env, lignes);
|
|
1148
1217
|
lignes.push(`</${balise}>`);
|
|
1149
1218
|
const bas = noeud.id !== undefined ? this.basDesBlocs.get(noeud.id) : undefined;
|
|
@@ -1192,6 +1261,27 @@ ${prise}${code}}
|
|
|
1192
1261
|
return undefined;
|
|
1193
1262
|
}
|
|
1194
1263
|
}
|
|
1264
|
+
contenuAccentJs(valeur, genre) {
|
|
1265
|
+
if (valeur.type !== 'texte' || !['titre', 'soustitre', 'texte'].includes(genre))
|
|
1266
|
+
return contenuJs(valeur, this.contexte);
|
|
1267
|
+
const parties = (liste) => liste.map((p) => 'texte' in p
|
|
1268
|
+
? p.accent ? `creer('span', { class: 'humalis-accent' }, ${chaine(p.texte)})` : p.code ? `creer('code', { class: 'humalis-code-ligne' }, ${chaine(p.texte)})` : chaine(p.texte)
|
|
1269
|
+
: contenuJs({ type: 'texte', parties: [p] }, this.contexte)).join(', ');
|
|
1270
|
+
const accent = (liste) => liste.some((p) => 'texte' in p && (p.accent || p.code));
|
|
1271
|
+
if (!accent(valeur.parties) && !valeur.versions?.some((v) => accent(v.parties)))
|
|
1272
|
+
return contenuJs(valeur, this.contexte);
|
|
1273
|
+
if (valeur.versions)
|
|
1274
|
+
return `...(${valeur.versions.slice(1).map((v) => `langue() === ${chaine(v.code)} ? [${parties(v.parties)}] : `).join('')}[${parties(valeur.versions[0].parties)}])`;
|
|
1275
|
+
return parties(valeur.parties);
|
|
1276
|
+
}
|
|
1277
|
+
contenuAccentHtml(valeur, genre, env) {
|
|
1278
|
+
if (valeur.type !== 'texte' || !['titre', 'soustitre', 'texte'].includes(genre) || !valeur.parties.some((p) => 'texte' in p && (p.accent || p.code)))
|
|
1279
|
+
return echapperTexte(texteDe(valeur, env, this.contexte.champs));
|
|
1280
|
+
return valeur.parties.map((p) => {
|
|
1281
|
+
const texte = echapperTexte('texte' in p ? p.texte : texteDe({ type: 'texte', parties: [p] }, env, this.contexte.champs));
|
|
1282
|
+
return 'texte' in p && p.accent ? `<span class="humalis-accent">${texte}</span>` : 'texte' in p && p.code ? `<code class="humalis-code-ligne">${texte}</code>` : texte;
|
|
1283
|
+
}).join('');
|
|
1284
|
+
}
|
|
1195
1285
|
texteInitial(valeur, env) {
|
|
1196
1286
|
try {
|
|
1197
1287
|
return texteDe(valeur, env, this.contexte.champs);
|
|
@@ -1456,7 +1546,7 @@ ${prise}${code}}
|
|
|
1456
1546
|
const classes = this.classes(noeud) || undefined;
|
|
1457
1547
|
const commun = (avant = []) => attributs([...avant, ['data-humalis', noeud.id], ['class', classes], ...(noeud.annonce ? [['aria-live', 'polite'], ['aria-atomic', 'true']] : []), ...attributsRaccourci(noeud)]);
|
|
1458
1548
|
const lucideHtml = noeud.reglages?.icone ? chercherIcone(noeud.reglages.icone)?.lucide : undefined;
|
|
1459
|
-
const contenu = (lucideHtml ? iconeHtml(lucideHtml, 'humalis-icone') : '') +
|
|
1549
|
+
const contenu = (lucideHtml ? iconeHtml(lucideHtml, 'humalis-icone') : '') + this.contenuAccentHtml(noeud.contenu, noeud.genre, env);
|
|
1460
1550
|
switch (noeud.genre) {
|
|
1461
1551
|
case 'titre':
|
|
1462
1552
|
case 'soustitre': {
|
|
@@ -1485,6 +1575,8 @@ ${prise}${code}}
|
|
|
1485
1575
|
}
|
|
1486
1576
|
}
|
|
1487
1577
|
}
|
|
1578
|
+
/** « menu repliable » : un bouton « Menu » le replie sur un petit écran. */
|
|
1579
|
+
const estRepliable = (noeud) => noeud.options.some((o) => o.nom === 'menu' && o.valeur === 'repliable');
|
|
1488
1580
|
/** Les blocs d'éléments d'écran, décrits par widgets.ts. */
|
|
1489
1581
|
const COMPOSITES = new Set(['onglets', 'deroulant', 'accordeon', 'carrousel', 'filAriane']);
|
|
1490
1582
|
/** La balise de chaque bloc : une section, l'en-tête et le pied de la page, sinon une boîte. */
|
|
@@ -1512,6 +1604,7 @@ const OUTILS_PAGE = [
|
|
|
1512
1604
|
'afficherMessage',
|
|
1513
1605
|
'planifier',
|
|
1514
1606
|
'mouvementReduit',
|
|
1607
|
+
'themeSombre',
|
|
1515
1608
|
'copier',
|
|
1516
1609
|
'partager',
|
|
1517
1610
|
'derniereRequete',
|
|
@@ -133,6 +133,10 @@ ${fonctions.join('\n')}`);
|
|
|
133
133
|
const emploie = (motif) => scriptsDesPages.some((contenu) => motif.test(contenu));
|
|
134
134
|
const interactions = { pressePapier: emploie(/\b(copier|partager)\(/), fenetres: emploie(/\bfenetre\(/), questions: emploie(/\bconfirmer\(/), bas: emploie(/\bonbas:/) };
|
|
135
135
|
fichiers.set('humalis/humalis.js', humalisJs(options.source, pagesFichiers, serveur, session, bascule ? modeTheme ?? '' : undefined, usage, application.langues, interactions));
|
|
136
|
+
// « menu repliable » : un seul écouteur pour tous les menus, ceux du HTML de départ comme ceux redessinés.
|
|
137
|
+
if ([...fichiers.values()].some((contenu) => contenu.includes('humalis-menu-bouton'))) {
|
|
138
|
+
fichiers.set('humalis/humalis.js', `${fichiers.get('humalis/humalis.js')}\n${MENU_REPLIABLE_JS}`);
|
|
139
|
+
}
|
|
136
140
|
// L'observateur des animations « au defilement », seulement si une page en a : il pose humalis-js au chargement.
|
|
137
141
|
if (animeAuDefilement([...fichiers.values()])) {
|
|
138
142
|
fichiers.set('humalis/humalis.js', `${fichiers.get('humalis/humalis.js')}\n// Les animations « au defilement » attendent que l'élément entre à l'écran.\n${OBSERVATEUR_DEFILEMENT}\nobserverLeDefilement();\n`);
|
|
@@ -291,6 +295,7 @@ function humalisJs(source, pages, serveur, session, theme, elements, langues, in
|
|
|
291
295
|
'afficherMessage',
|
|
292
296
|
'planifier',
|
|
293
297
|
'mouvementReduit',
|
|
298
|
+
'themeSombre',
|
|
294
299
|
'cliquer',
|
|
295
300
|
'focaliser',
|
|
296
301
|
'retirerPremier',
|
|
@@ -544,6 +549,14 @@ ${interactions.fenetres ? ' // Une fenêtre fermée rend le focus à ce qui l
|
|
|
544
549
|
// Le réglage change pendant la visite : la page est redessinée.
|
|
545
550
|
animationsReduites?.addEventListener('change', () => actualiser());
|
|
546
551
|
|
|
552
|
+
/** « theme est sombre » : le thème affiché (choix du visiteur ou du projet, sinon le système). */
|
|
553
|
+
const schemaSombre = globalThis.matchMedia?.('(prefers-color-scheme: dark)');
|
|
554
|
+
const themeSombre = () => {
|
|
555
|
+
const choisi = document.documentElement.dataset.theme;
|
|
556
|
+
return choisi ? choisi === 'sombre' : (schemaSombre?.matches ?? false);
|
|
557
|
+
};
|
|
558
|
+
schemaSombre?.addEventListener('change', () => actualiser());
|
|
559
|
+
|
|
547
560
|
/** Les minuteries de la page existent tant que leur condition est vraie ; « arreter » les arrête pour de bon. */
|
|
548
561
|
function reglerMinuteries() {
|
|
549
562
|
for (const m of page.minuteries ?? []) {
|
|
@@ -1112,6 +1125,11 @@ const themeJs = (defaut) => `
|
|
|
1112
1125
|
const THEME_PAR_DEFAUT = ${defaut ? `'${defaut}'` : "''"};
|
|
1113
1126
|
|
|
1114
1127
|
function definirTheme(theme) {
|
|
1128
|
+
// « basculer theme » : l'inverse du thème affiché (choix du visiteur ou du projet, sinon le système).
|
|
1129
|
+
if (theme === 'bascule') {
|
|
1130
|
+
const affiche = document.documentElement.dataset.theme || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'sombre' : 'clair');
|
|
1131
|
+
theme = affiche === 'sombre' ? 'clair' : 'sombre';
|
|
1132
|
+
}
|
|
1115
1133
|
try {
|
|
1116
1134
|
if (theme === 'automatique') localStorage.removeItem('${CLE_THEME}');
|
|
1117
1135
|
else localStorage.setItem('${CLE_THEME}', theme);
|
|
@@ -1121,5 +1139,33 @@ const themeJs = (defaut) => `
|
|
|
1121
1139
|
const choisi = theme === 'automatique' ? THEME_PAR_DEFAUT : theme;
|
|
1122
1140
|
if (choisi) document.documentElement.dataset.theme = choisi;
|
|
1123
1141
|
else delete document.documentElement.dataset.theme;
|
|
1142
|
+
// « theme est sombre » est relu : la page est redessinée.
|
|
1143
|
+
actualiser();
|
|
1124
1144
|
}
|
|
1125
1145
|
`;
|
|
1146
|
+
/** « menu repliable » : le bouton « Menu » ouvre et ferme les liens ; un lien choisi referme le menu. */
|
|
1147
|
+
const MENU_REPLIABLE_JS = `// Les menus repliables : le bouton « Menu » d'un petit écran.
|
|
1148
|
+
document.addEventListener('click', (evenement) => {
|
|
1149
|
+
const cible = evenement.target instanceof Element ? evenement.target : null;
|
|
1150
|
+
const menu = cible?.closest('.humalis-menu-repliable');
|
|
1151
|
+
if (!menu || !cible.closest('.humalis-menu-bouton, a')) return;
|
|
1152
|
+
const ouvrir = cible.closest('.humalis-menu-bouton') !== null && !menu.hasAttribute('data-ouvert');
|
|
1153
|
+
menu.toggleAttribute('data-ouvert', ouvrir);
|
|
1154
|
+
menu.querySelector(':scope > .humalis-menu-bouton')?.setAttribute('aria-expanded', String(ouvrir));
|
|
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
|
+
});
|
|
1171
|
+
`;
|
|
@@ -133,6 +133,9 @@ export function expressionJs(valeur, contexte) {
|
|
|
133
133
|
case 'mouvementReduit':
|
|
134
134
|
// « réduire les animations » du système (humalis.js), relu à chaque dessin.
|
|
135
135
|
return 'mouvementReduit()';
|
|
136
|
+
case 'themeSombre':
|
|
137
|
+
// Le thème affiché (humalis.js), relu à chaque dessin.
|
|
138
|
+
return 'themeSombre()';
|
|
136
139
|
case 'etatRequete':
|
|
137
140
|
case 'erreurRequete':
|
|
138
141
|
// Les sources ne sont pas encore produites pour le site statique : l'analyse de la cible les refuse avant.
|
|
@@ -371,8 +374,9 @@ export function evaluer(valeur, env) {
|
|
|
371
374
|
}
|
|
372
375
|
case 'utilisateur':
|
|
373
376
|
case 'etatRequete':
|
|
374
|
-
// Le HTML de départ n'écrit aucun mouvement.
|
|
377
|
+
// Le HTML de départ n'écrit aucun mouvement, et ne connaît pas le thème du visiteur.
|
|
375
378
|
case 'mouvementReduit':
|
|
379
|
+
case 'themeSombre':
|
|
376
380
|
return false;
|
|
377
381
|
case 'erreurRequete':
|
|
378
382
|
return '';
|
|
@@ -34,9 +34,39 @@ export function reglagesUtilises(codes) {
|
|
|
34
34
|
export function cssEcran(codes) {
|
|
35
35
|
const utilises = reglagesUtilises(codes);
|
|
36
36
|
const regles = [];
|
|
37
|
+
if (codes.some((c) => c.includes('humalis-accent')))
|
|
38
|
+
regles.push('.humalis-accent { color: var(--humalis-marque, currentColor); }');
|
|
39
|
+
if (codes.some((c) => c.includes('humalis-code-ligne')))
|
|
40
|
+
regles.push('.humalis-code-ligne { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; padding: 0.1em 0.25em; border-radius: 0.2em; background: rgb(127 127 127 / 0.12); }');
|
|
41
|
+
if (codes.some((c) => c.includes('humalis-bloc-code')))
|
|
42
|
+
regles.push('.humalis-bloc-code-conteneur { position: relative; max-width: 100%; overflow-x: auto; } .humalis-bloc-code { overflow-x: auto; padding: 1rem; border-radius: 0.5rem; background: rgb(127 127 127 / 0.12); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre; } .humalis-bloc-code-conteneur button { position: absolute; top: 0.5rem; right: 0.5rem; }');
|
|
43
|
+
if (codes.some((c) => c.includes('humalis-code-mot')))
|
|
44
|
+
regles.push('.humalis-code-mot { color: #7348c3; font-weight: 600; } .humalis-code-texte { color: #147d50; } .humalis-code-commentaire { color: #68717d; } .humalis-code-nombre { color: #b25215; }');
|
|
45
|
+
if (codes.some((c) => c.includes('humalis-tableau-conteneur')))
|
|
46
|
+
regles.push('.humalis-tableau-conteneur { max-width: 100%; overflow-x: auto; } .humalis-tableau-conteneur table { border-collapse: collapse; width: 100%; } .humalis-tableau-conteneur th, .humalis-tableau-conteneur td { border: 1px solid rgb(127 127 127 / 0.3); padding: 0.5rem; text-align: left; } .humalis-tableau-conteneur caption { text-align: left; font-weight: 700; margin-bottom: 0.5rem; }');
|
|
47
|
+
if (codes.some((c) => c.includes('humalis-liste')))
|
|
48
|
+
regles.push('.humalis-liste { padding-inline-start: 1.5rem; } .humalis-liste > li { margin-block: 0.35rem; }');
|
|
37
49
|
// L'en-tête et le pied communs : hors de la page, sur toute la largeur.
|
|
38
|
-
if (codes.some((c) => /humalis-entete\b/.test(c)))
|
|
50
|
+
if (codes.some((c) => /humalis-entete\b/.test(c))) {
|
|
39
51
|
regles.push('.humalis-entete { padding: 0.75rem 1rem; border-bottom: 1px solid rgb(138 138 144 / 0.4); }');
|
|
52
|
+
// « Aller au contenu » : hors de l'écran, il apparaît quand le clavier l'atteint.
|
|
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
|
+
regles.push('.humalis-aller-au-contenu:focus { top: 0.5rem; }');
|
|
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; }');
|
|
60
|
+
// « menu repliable » : sur un petit écran, les liens attendent le bouton « Menu ».
|
|
61
|
+
if (codes.some((c) => c.includes('humalis-menu-repliable'))) {
|
|
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); }');
|
|
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; } }');
|
|
69
|
+
}
|
|
40
70
|
if (codes.some((c) => /humalis-pied\b/.test(c)))
|
|
41
71
|
regles.push('.humalis-pied { padding: 1.5rem 1rem; margin-top: 2rem; border-top: 1px solid rgb(138 138 144 / 0.4); }');
|
|
42
72
|
if (utilises.size === 0 && regles.length === 0)
|