humalis 1.0.4 → 1.1.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 +33 -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 +7 -2
- package/dist/cibles/mobile/rendu.js +6 -3
- package/dist/cibles/statique/page.js +99 -6
- package/dist/cibles/statique/projet.js +31 -0
- package/dist/cibles/statique/valeurs.js +5 -1
- package/dist/cibles/web/css-ecran.js +22 -1
- package/dist/cibles/web/ecran.js +161 -16
- package/dist/cibles/web/interactions.js +50 -3
- package/dist/cibles/web/projet.js +29 -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 +5274 -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 +85 -3
- package/dist/parser/theme.js +10 -1
- package/dist/ri/construire.js +24 -1
- package/dist/vocabulaire/table-interactions.js +16 -1
- package/dist/vocabulaire/table.js +4 -4
- package/docs/guide.en.md +170 -2
- package/docs/guide.md +171 -1
- package/llms.txt +14 -1
- package/package.json +4 -2
package/dist/cibles/web/ecran.js
CHANGED
|
@@ -2,6 +2,7 @@ import { verificationsDuChamp, messageEnCode, libellesEnLangues } from '../../i1
|
|
|
2
2
|
import { titreComplet, adresseImage, sansBarreFinale } from '../publication.js';
|
|
3
3
|
import { sousActions } from '../../ri/actions.js';
|
|
4
4
|
import { nomDeLaCopie, estFichierLocal } from '../../ri/medias.js';
|
|
5
|
+
import { estCombine } from '../../ri/types.js';
|
|
5
6
|
import { simplifier } from '../../analyse/elements.js';
|
|
6
7
|
import { lireCible } from '../../analyse/navigation.js';
|
|
7
8
|
import { Ecrivain } from './ecrivain.js';
|
|
@@ -10,6 +11,8 @@ import { WIDGETS_WEB } from './widgets.js';
|
|
|
10
11
|
import { baliseGraphique } from './graphique.js';
|
|
11
12
|
import { typeTS, typeDe, expression, contenuJSX, condition, ficheAvec } from './valeurs.js';
|
|
12
13
|
import { importsDesInteractions, decrireTouches } from './interactions.js';
|
|
14
|
+
import { colorerCodeHumalis } from '../commun/colorerCode.js';
|
|
15
|
+
import { attributsFrappe } from '../commun/mouvement.js';
|
|
13
16
|
/** Les sortes de champ où l'on écrit une seule ligne : la touche Entrée y envoie le formulaire. */
|
|
14
17
|
const UNE_LIGNE = new Set(['texte', 'email', 'telephone', 'nombre', 'date', 'heure', 'motDePasse', 'adresseWeb']);
|
|
15
18
|
/** Un texte écrit en clair dans du JSX : tel quel, ou entre accolades s'il contient un caractère du JSX. */
|
|
@@ -152,7 +155,9 @@ export const RENDU_WEB = {
|
|
|
152
155
|
ouvrir: (genre, options, classes, ancre) => {
|
|
153
156
|
const toutes = [`humalis-${genre}`, ...options.map((o) => `humalis-${o.nom}-${o.valeur}`), ...classes];
|
|
154
157
|
const balise = genre === 'section' ? 'section' : genre === 'menu' ? 'nav' : 'div';
|
|
155
|
-
|
|
158
|
+
// « menu repliable » : le bouton « Menu » du petit écran (src/humalis/menu.ts).
|
|
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>' : ''}`;
|
|
156
161
|
},
|
|
157
162
|
fermer: (genre) => (genre === 'section' ? '</section>' : genre === 'menu' ? '</nav>' : '</div>'),
|
|
158
163
|
},
|
|
@@ -204,6 +209,7 @@ class GenerateurEcran {
|
|
|
204
209
|
chemins;
|
|
205
210
|
application;
|
|
206
211
|
parametres;
|
|
212
|
+
listeEnCours;
|
|
207
213
|
contexte = { types: new Map(), champs: new Map(), sortes: new Map() };
|
|
208
214
|
/** Les champs qui ont une sorte ou des options, et le libellé de chaque champ (pour les messages). */
|
|
209
215
|
saisies = new Map();
|
|
@@ -325,7 +331,7 @@ class GenerateurEcran {
|
|
|
325
331
|
for (const g of this.ecran.gestionnaires)
|
|
326
332
|
this.enregistrer(g, []);
|
|
327
333
|
const composant = pascal(this.ecran.nom);
|
|
328
|
-
const { titre, description } = titreEtDescription(this.ecran);
|
|
334
|
+
const { titre, description } = titreEtDescription(this.ecran, this.application.langues);
|
|
329
335
|
// Un composant reçoit ses paramètres comme des propriétés React, et ne
|
|
330
336
|
// change pas le titre de la page qui l'affiche.
|
|
331
337
|
const signature = this.parametres
|
|
@@ -392,7 +398,9 @@ class GenerateurEcran {
|
|
|
392
398
|
const lues = [...new Set([...code.matchAll(/\bactuel\.current\.(\w+)/g)].map((m) => m[1]))];
|
|
393
399
|
code = code.replace(' /*ACTUEL*/\n', lues.length > 0 ? ` const actuel = useActuel({ ${lues.join(', ')} });\n` : '');
|
|
394
400
|
// « mouvement est reduit » : suivi en direct (src/humalis/temps.ts).
|
|
395
|
-
code = code.replace(' /*MOUVEMENT*/\n', /\bmouvementReduit\b/.test(code) ? ' const mouvementReduit = useMouvementReduit();\n' : '')
|
|
401
|
+
code = code.replace(' /*MOUVEMENT*/\n', (/\bmouvementReduit\b/.test(code) ? ' const mouvementReduit = useMouvementReduit();\n' : '') +
|
|
402
|
+
// « theme est sombre » : le thème affiché, suivi en direct.
|
|
403
|
+
(/\bthemeSombre\b/.test(code) ? ' const themeSombre = useThemeSombre();\n' : ''));
|
|
396
404
|
// Sur le téléphone, « confirmer » vient d'un crochet : la question est une fenêtre de l'application.
|
|
397
405
|
code = code.replace(' /*CONFIRMER*/\n', this.rendu.confirmationParCrochet && /\bconfirmer\(/.test(code) ? ' const confirmer = useConfirmer();\n' : '');
|
|
398
406
|
code = code.replace(' /*PLANIFIER*/\n', (/\bplanifier\(/.test(code) ? ' const planifier = usePlanifier();\n' : '') + (/\bderniereRequete\(/.test(code) ? ' const derniereRequete = useDerniere();\n' : ''));
|
|
@@ -407,13 +415,16 @@ class GenerateurEcran {
|
|
|
407
415
|
}
|
|
408
416
|
/** Le premier titre (ou texte) de la page, s'il a des versions : il suit la langue (« t({ fr, en }) »). */
|
|
409
417
|
traduit(genre) {
|
|
410
|
-
const valeur = this.application.langues ? premiereValeur(genre, this.ecran.vue) : undefined;
|
|
418
|
+
const valeur = this.application.langues ? premiereValeur(genre, this.ecran.vue, this.application.langues) : undefined;
|
|
411
419
|
return valeur?.type === 'texte' && valeur.versions ? expression(valeur, this.contexte) : undefined;
|
|
412
420
|
}
|
|
413
421
|
// ─── Recensement : champs, cibles des événements, besoins ───────────────
|
|
414
422
|
recenser(noeuds, boucles, formulaire) {
|
|
415
423
|
for (const noeud of noeuds) {
|
|
416
424
|
switch (noeud.type) {
|
|
425
|
+
case 'codeDocumentation':
|
|
426
|
+
this.besoins.messages = true;
|
|
427
|
+
break;
|
|
417
428
|
case 'element':
|
|
418
429
|
if (noeud.genre === 'champ') {
|
|
419
430
|
const libelle = texteDeBase(noeud.contenu) ?? noeud.id;
|
|
@@ -834,8 +845,8 @@ class GenerateurEcran {
|
|
|
834
845
|
const code = expression(valeur, this.contexte);
|
|
835
846
|
return complet && site?.nom ? `${code} + ${suffixe()}` : code;
|
|
836
847
|
};
|
|
837
|
-
const titreCode = enCode(apercu?.titre ?? premiereValeur('titre', this.ecran.vue), true) ?? titreLitteral(titre);
|
|
838
|
-
const descriptionCode = enCode(apercu?.description ?? premiereValeur('texte', this.ecran.vue)) ??
|
|
848
|
+
const titreCode = enCode(apercu?.titre ?? premiereValeur('titre', this.ecran.vue, this.application.langues), true) ?? titreLitteral(titre);
|
|
849
|
+
const descriptionCode = enCode(apercu?.description ?? premiereValeur('texte', this.ecran.vue, this.application.langues)) ??
|
|
839
850
|
(site?.descriptionLangues ? messageEnCode(site.descriptionLangues, chaine) : chaine(site?.description ?? description));
|
|
840
851
|
const chemin = this.chemins.get(this.ecran.nom) ?? `/${simplifier(this.ecran.nom)}`;
|
|
841
852
|
const image = apercu?.image ?? (site?.image !== undefined ? { type: 'texte', parties: [{ texte: site.image }] } : undefined);
|
|
@@ -1017,7 +1028,7 @@ class GenerateurEcran {
|
|
|
1017
1028
|
break;
|
|
1018
1029
|
case 'copie':
|
|
1019
1030
|
// « copier code » : le résultat est dit à la personne, sauf « sans message ».
|
|
1020
|
-
e.ligne(`await copier(${this.enTexte(action.valeur)}${action.message === null ? '' : `, afficherMessage${action.message === 'Copié' ? '' : `, ${chaine(action.message)}`}`});`);
|
|
1031
|
+
e.ligne(`await copier(${this.enTexte(action.valeur)}${action.message === null ? '' : `, afficherMessage${action.texte ? `, ${x(action.texte)}` : action.message === 'Copié' ? '' : `, ${chaine(action.message)}`}`});`);
|
|
1021
1032
|
break;
|
|
1022
1033
|
case 'partage': {
|
|
1023
1034
|
// La feuille de partage du système ; sans elle, le lien (ou le texte) est copié.
|
|
@@ -1437,6 +1448,50 @@ class GenerateurEcran {
|
|
|
1437
1448
|
ecrireVue(e, noeuds) {
|
|
1438
1449
|
for (const noeud of noeuds) {
|
|
1439
1450
|
switch (noeud.type) {
|
|
1451
|
+
case 'tableauDocumentation':
|
|
1452
|
+
if (this.application.rendu) {
|
|
1453
|
+
const mobile = (valeur) => contenuJSX(valeur, this.contexte)
|
|
1454
|
+
.replace(/<code className="humalis-code-ligne">/g, '<Text style={{ fontFamily: "monospace" }}>')
|
|
1455
|
+
.replace(/<span className="humalis-accent">/g, '<Text style={s.accent}>')
|
|
1456
|
+
.replace(/<\/(code|span)>/g, '</Text>');
|
|
1457
|
+
e.ligne('<ScrollView horizontal><View>');
|
|
1458
|
+
e.ligne(`<Text>${mobile(noeud.legende)}</Text>`);
|
|
1459
|
+
for (const ligne of [noeud.colonnes, ...noeud.rangees]) {
|
|
1460
|
+
e.ligne('<View style={{ flexDirection: "row" }}>');
|
|
1461
|
+
for (const cellule of ligne)
|
|
1462
|
+
e.ligne(`<Text style={{ minWidth: 120 }}>${mobile(cellule)}</Text>`);
|
|
1463
|
+
e.ligne('</View>');
|
|
1464
|
+
}
|
|
1465
|
+
e.ligne('</View></ScrollView>');
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
e.ligne('<div className="humalis-tableau-conteneur"><table><caption>');
|
|
1469
|
+
e.ligne(contenuJSX(noeud.legende, this.contexte));
|
|
1470
|
+
e.ligne('</caption><thead><tr>');
|
|
1471
|
+
for (const cellule of noeud.colonnes)
|
|
1472
|
+
e.ligne(`<th scope="col">${contenuJSX(cellule, this.contexte)}</th>`);
|
|
1473
|
+
e.ligne('</tr></thead><tbody>');
|
|
1474
|
+
for (const ligne of noeud.rangees) {
|
|
1475
|
+
e.ligne('<tr>');
|
|
1476
|
+
for (const cellule of ligne)
|
|
1477
|
+
e.ligne(`<td>${contenuJSX(cellule, this.contexte)}</td>`);
|
|
1478
|
+
e.ligne('</tr>');
|
|
1479
|
+
}
|
|
1480
|
+
e.ligne('</tbody></table></div>');
|
|
1481
|
+
}
|
|
1482
|
+
break;
|
|
1483
|
+
case 'codeDocumentation':
|
|
1484
|
+
if (this.application.rendu)
|
|
1485
|
+
e.ligne(`<ScrollView horizontal><Text style={{ fontFamily: 'monospace' }}>{${chaine(noeud.code)}}</Text></ScrollView>`);
|
|
1486
|
+
else {
|
|
1487
|
+
const copie = this.application.langues ? `t({ fr: 'Copié', en: 'Copied' })` : `'Copié'`;
|
|
1488
|
+
const libelle = this.application.langues ? `{t({ fr: 'Copier', en: 'Copy' })}` : 'Copier';
|
|
1489
|
+
const frappe = attributsFrappe(noeud);
|
|
1490
|
+
const animation = frappe ? ` ${frappe.classe}` : '';
|
|
1491
|
+
const marques = frappe ? ` data-humalis-frappe="${frappe.frappe}" data-humalis-frappe-cle="${frappe.cle}"` : '';
|
|
1492
|
+
e.ligne(`<div className="humalis-bloc-code-conteneur"><pre className="humalis-bloc-code${animation}" data-langage=${chaine(noeud.langage ?? '')}${marques}><code dangerouslySetInnerHTML={{ __html: ${chaine(colorerCodeHumalis(noeud.code, noeud.langage))} }} /></pre><button type="button" onClick={() => void copier(${chaine(noeud.code)}, afficherMessage, ${copie})}>${libelle}</button></div>`);
|
|
1493
|
+
}
|
|
1494
|
+
break;
|
|
1440
1495
|
case 'element':
|
|
1441
1496
|
this.ecrireElement(e, noeud);
|
|
1442
1497
|
break;
|
|
@@ -1500,7 +1555,13 @@ class GenerateurEcran {
|
|
|
1500
1555
|
e.ligne(`{${liste}.map((${variable}, ${index}) => (`);
|
|
1501
1556
|
e.indenter(() => {
|
|
1502
1557
|
e.ligne(`<Fragment key={${index}}>`);
|
|
1503
|
-
e.indenter(() =>
|
|
1558
|
+
e.indenter(() => {
|
|
1559
|
+
if (this.listeEnCours)
|
|
1560
|
+
e.ligne(this.application.rendu ? `<View style={{ flexDirection: "row" }}><Text>${this.listeEnCours === 'liste' ? '•' : `{${index} + 1}.`}</Text>` : '<li>');
|
|
1561
|
+
this.ecrireVue(e, contenu);
|
|
1562
|
+
if (this.listeEnCours)
|
|
1563
|
+
e.ligne(this.application.rendu ? '</View>' : '</li>');
|
|
1564
|
+
});
|
|
1504
1565
|
e.ligne('</Fragment>');
|
|
1505
1566
|
});
|
|
1506
1567
|
e.ligne('))}');
|
|
@@ -1541,6 +1602,40 @@ class GenerateurEcran {
|
|
|
1541
1602
|
break;
|
|
1542
1603
|
}
|
|
1543
1604
|
case 'bloc':
|
|
1605
|
+
if (noeud.genre === 'liste' || noeud.genre === 'listeNumerotee') {
|
|
1606
|
+
if (this.application.rendu) {
|
|
1607
|
+
e.ligne('<View>');
|
|
1608
|
+
e.indenter(() => noeud.corps.forEach((enfant, index) => {
|
|
1609
|
+
if (enfant.type === 'repetition') {
|
|
1610
|
+
this.listeEnCours = noeud.genre;
|
|
1611
|
+
this.ecrireVue(e, [enfant]);
|
|
1612
|
+
this.listeEnCours = undefined;
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
e.ligne('<View style={{ flexDirection: "row" }}>');
|
|
1616
|
+
e.ligne(`<Text>${noeud.genre === 'liste' ? '•' : `${index + 1}.`}</Text>`);
|
|
1617
|
+
this.ecrireVue(e, [enfant]);
|
|
1618
|
+
e.ligne('</View>');
|
|
1619
|
+
}));
|
|
1620
|
+
e.ligne('</View>');
|
|
1621
|
+
break;
|
|
1622
|
+
}
|
|
1623
|
+
const balise = noeud.genre === 'liste' ? 'ul' : 'ol';
|
|
1624
|
+
e.ligne(`<${balise} className="humalis-liste">`);
|
|
1625
|
+
e.indenter(() => noeud.corps.forEach((enfant) => {
|
|
1626
|
+
if (enfant.type === 'repetition') {
|
|
1627
|
+
this.listeEnCours = noeud.genre;
|
|
1628
|
+
this.ecrireVue(e, [enfant]);
|
|
1629
|
+
this.listeEnCours = undefined;
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
e.ligne('<li>');
|
|
1633
|
+
e.indenter(() => this.ecrireVue(e, [enfant]));
|
|
1634
|
+
e.ligne('</li>');
|
|
1635
|
+
}));
|
|
1636
|
+
e.ligne(`</${balise}>`);
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1544
1639
|
if (COMPOSITES.has(noeud.genre)) {
|
|
1545
1640
|
this.ecrireComposite(e, noeud);
|
|
1546
1641
|
break;
|
|
@@ -1926,24 +2021,72 @@ function modifiees(actions, noms = new Set()) {
|
|
|
1926
2021
|
}
|
|
1927
2022
|
return noms;
|
|
1928
2023
|
}
|
|
1929
|
-
/**
|
|
1930
|
-
|
|
2024
|
+
/**
|
|
2025
|
+
* « si langue est anglais » : la branche montrée dans la langue `code` ;
|
|
2026
|
+
* rien pour une condition qui ne porte pas sur la langue.
|
|
2027
|
+
*/
|
|
2028
|
+
function brancheDeLangue(noeud, code) {
|
|
2029
|
+
if (noeud.type !== 'condition')
|
|
2030
|
+
return undefined;
|
|
2031
|
+
const test = noeud.test;
|
|
2032
|
+
if (estCombine(test))
|
|
2033
|
+
return undefined;
|
|
2034
|
+
if (test.gauche.type !== 'variable' || test.gauche.nom !== 'langue' || (test.operateur !== 'egal' && test.operateur !== 'different'))
|
|
2035
|
+
return undefined;
|
|
2036
|
+
const visee = test.droite && texteDeBase(test.droite);
|
|
2037
|
+
if (visee === undefined)
|
|
2038
|
+
return undefined;
|
|
2039
|
+
const vraie = (visee === code) !== (test.operateur === 'different') !== Boolean(test.negation);
|
|
2040
|
+
return vraie ? noeud.alors : noeud.sinon;
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* La première valeur d'un élément de ce genre, affiché sans condition (même
|
|
2044
|
+
* dans un bloc de mise en page), dans la langue `code` : avec des langues,
|
|
2045
|
+
* une branche « si langue est … » compte.
|
|
2046
|
+
*/
|
|
2047
|
+
function premiereValeurEn(genre, noeuds, code) {
|
|
1931
2048
|
for (const n of noeuds) {
|
|
1932
2049
|
if (n.type === 'element' && n.genre === genre)
|
|
1933
2050
|
return n.contenu;
|
|
1934
|
-
|
|
1935
|
-
|
|
2051
|
+
const corps = n.type === 'bloc' ? n.corps : code !== undefined ? brancheDeLangue(n, code) : undefined;
|
|
2052
|
+
if (corps) {
|
|
2053
|
+
const trouvee = premiereValeurEn(genre, corps, code);
|
|
1936
2054
|
if (trouvee)
|
|
1937
2055
|
return trouvee;
|
|
1938
2056
|
}
|
|
1939
2057
|
}
|
|
1940
2058
|
return undefined;
|
|
1941
2059
|
}
|
|
2060
|
+
/**
|
|
2061
|
+
* La première valeur d'un élément de ce genre. Avec des langues, celle de la
|
|
2062
|
+
* langue de base ; si les branches « si langue est … » en montrent une autre
|
|
2063
|
+
* dans une autre langue, elle devient la version de cette langue.
|
|
2064
|
+
*/
|
|
2065
|
+
function premiereValeur(genre, noeuds, langues) {
|
|
2066
|
+
if (!langues || langues.length === 0)
|
|
2067
|
+
return premiereValeurEn(genre, noeuds);
|
|
2068
|
+
const [base, ...autres] = langues;
|
|
2069
|
+
const valeur = premiereValeurEn(genre, noeuds, base.code);
|
|
2070
|
+
if (valeur?.type !== 'texte')
|
|
2071
|
+
return valeur;
|
|
2072
|
+
const versions = autres.flatMap(({ code }) => {
|
|
2073
|
+
const autre = premiereValeurEn(genre, noeuds, code);
|
|
2074
|
+
if (autre === valeur || autre?.type !== 'texte')
|
|
2075
|
+
return [];
|
|
2076
|
+
return [{ code, parties: autre.versions?.find((v) => v.code === code)?.parties ?? autre.parties }];
|
|
2077
|
+
});
|
|
2078
|
+
if (versions.length === 0)
|
|
2079
|
+
return valeur;
|
|
2080
|
+
// Les versions écrites avec « en anglais » restent, celles des branches les complètent.
|
|
2081
|
+
const ecrites = (valeur.versions ?? []).filter((v) => v.code !== base.code && !versions.some((b) => b.code === v.code));
|
|
2082
|
+
return { ...valeur, versions: [{ code: base.code, parties: valeur.parties }, ...ecrites, ...versions] };
|
|
2083
|
+
}
|
|
1942
2084
|
/**
|
|
1943
2085
|
* Le titre et la description de la page (décision W7) : le premier titre
|
|
1944
2086
|
* et le premier texte écrits en clair, à défaut le nom de la page.
|
|
1945
2087
|
*/
|
|
1946
|
-
export function titreEtDescription(ecran) {
|
|
2088
|
+
export function titreEtDescription(ecran, langues) {
|
|
2089
|
+
const base = langues?.[0]?.code;
|
|
1947
2090
|
const premier = (genre, noeuds) => {
|
|
1948
2091
|
for (const n of noeuds) {
|
|
1949
2092
|
if (n.type === 'element' && n.genre === genre) {
|
|
@@ -1951,9 +2094,11 @@ export function titreEtDescription(ecran) {
|
|
|
1951
2094
|
if (texte)
|
|
1952
2095
|
return texte;
|
|
1953
2096
|
}
|
|
1954
|
-
// Toujours affiché : le contenu d'un bloc de mise en page compte aussi
|
|
1955
|
-
|
|
1956
|
-
|
|
2097
|
+
// Toujours affiché : le contenu d'un bloc de mise en page compte aussi, et
|
|
2098
|
+
// avec des langues, la branche « si langue est … » de la langue de base.
|
|
2099
|
+
const corps = n.type === 'bloc' ? n.corps : base !== undefined ? brancheDeLangue(n, base) : undefined;
|
|
2100
|
+
if (corps) {
|
|
2101
|
+
const texte = premier(genre, corps);
|
|
1957
2102
|
if (texte)
|
|
1958
2103
|
return texte;
|
|
1959
2104
|
}
|
|
@@ -39,12 +39,15 @@ export function fichiersDesInteractions(codes, plateforme = 'web') {
|
|
|
39
39
|
fichiers.set('voir-plus.ts', plateforme === 'web' ? VOIR_PLUS_TS : VOIR_PLUS_MOBILE);
|
|
40
40
|
if (emploie('deplacer'))
|
|
41
41
|
fichiers.set('deplacer.ts', plateforme === 'web' ? DEPLACER_TS : DEPLACER_MOBILE);
|
|
42
|
+
if (emploie('menu'))
|
|
43
|
+
fichiers.set('menu.ts', MENU_TS);
|
|
42
44
|
return fichiers;
|
|
43
45
|
}
|
|
44
46
|
/** Les outils de chaque fichier, pour écrire les imports d'une page. */
|
|
45
47
|
export const OUTILS_INTERACTIONS = {
|
|
46
|
-
temps: ['useActuel', 'useMinuterie', 'usePlanifier', 'useMouvementReduit', 'useQuandChange', 'useQuandPause', 'useDerniere'],
|
|
48
|
+
temps: ['useActuel', 'useMinuterie', 'usePlanifier', 'useMouvementReduit', 'useThemeSombre', 'useQuandChange', 'useQuandPause', 'useDerniere'],
|
|
47
49
|
'presse-papier': ['copier', 'partager', 'adresseAbsolue'],
|
|
50
|
+
menu: ['menuRepliable'],
|
|
48
51
|
clavier: ['useRaccourcis', 'correspond', 'cliquer', 'focaliser'],
|
|
49
52
|
confirmation: ['confirmer', 'useConfirmer'],
|
|
50
53
|
fenetre: ['Fenetre'],
|
|
@@ -57,7 +60,9 @@ export function importsDesInteractions(code, dossier, outils = OUTILS_INTERACTIO
|
|
|
57
60
|
for (const [fichier, noms] of Object.entries(outils)) {
|
|
58
61
|
// « confirmer » vient de useConfirmer() quand la page le déclare (le téléphone).
|
|
59
62
|
const declares = new Set([...code.matchAll(/\bconst (\w+) = use\w+\(/g)].map((m) => m[1]));
|
|
60
|
-
|
|
63
|
+
// Fenetre est un composant ; menuRepliable est passé tel quel à onClick.
|
|
64
|
+
const motif = (n) => (n === 'Fenetre' ? '<Fenetre\\b' : n === 'menuRepliable' ? '\\{menuRepliable\\}' : `\\b${n}\\(`);
|
|
65
|
+
const employes = noms.filter((n) => !declares.has(n) && new RegExp(motif(n)).test(code));
|
|
61
66
|
if (employes.length)
|
|
62
67
|
lignes.push(`import { ${employes.join(', ')} } from '${dossier}/${fichier}';`);
|
|
63
68
|
}
|
|
@@ -71,7 +76,7 @@ export function importsDesInteractions(code, dossier, outils = OUTILS_INTERACTIO
|
|
|
71
76
|
function tempsTs(plateforme) {
|
|
72
77
|
const web = plateforme === 'web';
|
|
73
78
|
return `import { useCallback, useEffect, useLayoutEffect, useRef, useState${web ? ', useSyncExternalStore' : ''} } from 'react';
|
|
74
|
-
${web ? '' : "import { AccessibilityInfo, AppState } from 'react-native';\n"}
|
|
79
|
+
${web ? '' : "import { AccessibilityInfo, AppState, useColorScheme } from 'react-native';\n"}
|
|
75
80
|
/**
|
|
76
81
|
* Les dernières valeurs de la page, pour ce qui s'exécute plus tard (une
|
|
77
82
|
* minuterie lancée par un clic, le bouton d'un message) : sans cela, il
|
|
@@ -195,6 +200,28 @@ export function useMouvementReduit(): boolean {
|
|
|
195
200
|
// Le prérendu n'écrit aucun mouvement.
|
|
196
201
|
() => false,
|
|
197
202
|
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** « theme est sombre » : le thème affiché (choix du visiteur ou du projet, sinon le système), suivi en direct. */
|
|
206
|
+
export function useThemeSombre(): boolean {
|
|
207
|
+
return useSyncExternalStore(
|
|
208
|
+
(changement) => {
|
|
209
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
210
|
+
media.addEventListener('change', changement);
|
|
211
|
+
const observateur = new MutationObserver(changement);
|
|
212
|
+
observateur.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
213
|
+
return () => {
|
|
214
|
+
media.removeEventListener('change', changement);
|
|
215
|
+
observateur.disconnect();
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
() => {
|
|
219
|
+
const choisi = document.documentElement.dataset.theme;
|
|
220
|
+
return choisi ? choisi === 'sombre' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
221
|
+
},
|
|
222
|
+
// Le prérendu ne connaît pas le thème du visiteur.
|
|
223
|
+
() => false,
|
|
224
|
+
);
|
|
198
225
|
}`
|
|
199
226
|
: `/** « mouvement est reduit » : la personne a demandé à son téléphone de réduire les animations, suivi en direct. */
|
|
200
227
|
export function useMouvementReduit(): boolean {
|
|
@@ -205,6 +232,11 @@ export function useMouvementReduit(): boolean {
|
|
|
205
232
|
return () => abonnement.remove();
|
|
206
233
|
}, []);
|
|
207
234
|
return reduit;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** « theme est sombre » : le thème affiché par le téléphone, suivi en direct. */
|
|
238
|
+
export function useThemeSombre(): boolean {
|
|
239
|
+
return useColorScheme() === 'dark';
|
|
208
240
|
}`}
|
|
209
241
|
|
|
210
242
|
/**
|
|
@@ -1041,3 +1073,18 @@ const CSS_DEPLACER = `.humalis-liste-deplacable { list-style: none; margin: 0; p
|
|
|
1041
1073
|
.humalis-insertion-avant { box-shadow: 0 -3px 0 var(--humalis-bleu); }
|
|
1042
1074
|
.humalis-insertion-apres { box-shadow: 0 3px 0 var(--humalis-bleu); }
|
|
1043
1075
|
${CSS_CACHE}`;
|
|
1076
|
+
/** `src/humalis/menu.ts` : « menu repliable », le bouton « Menu » d'un petit écran. */
|
|
1077
|
+
const MENU_TS = `/**
|
|
1078
|
+
* « menu repliable » : le bouton « Menu » ouvre et ferme les liens (visible
|
|
1079
|
+
* seulement sur un petit écran, par le CSS) ; un lien choisi referme le menu.
|
|
1080
|
+
*/
|
|
1081
|
+
export function menuRepliable(evenement: { target: EventTarget; currentTarget: EventTarget }): void {
|
|
1082
|
+
const menu = evenement.currentTarget as HTMLElement;
|
|
1083
|
+
const cible = evenement.target as Element;
|
|
1084
|
+
const bouton = menu.querySelector(':scope > .humalis-menu-bouton');
|
|
1085
|
+
const ouvrir = cible.closest('.humalis-menu-bouton') !== null && !menu.hasAttribute('data-ouvert');
|
|
1086
|
+
if (!ouvrir && cible.closest('.humalis-menu-bouton, a') === null) return;
|
|
1087
|
+
menu.toggleAttribute('data-ouvert', ouvrir);
|
|
1088
|
+
bouton?.setAttribute('aria-expanded', String(ouvrir));
|
|
1089
|
+
}
|
|
1090
|
+
`;
|
|
@@ -98,6 +98,9 @@ export function genererProjetWeb(application, options) {
|
|
|
98
98
|
}
|
|
99
99
|
if (dispositions.entete || dispositions.pied)
|
|
100
100
|
fichiers.set('src/Disposition.tsx', dispositionTsx(dispositions, application));
|
|
101
|
+
// Avec un en-tête, le clavier peut sauter directement au contenu de la page.
|
|
102
|
+
if (dispositions.entete)
|
|
103
|
+
fichiers.set('src/humalis/aller-au-contenu.tsx', ALLER_AU_CONTENU);
|
|
101
104
|
const pages = application.ecrans.map((ecran) => ({ ecran, page: genererEcran(ecran, chemins, contexte) }));
|
|
102
105
|
session ||= pages.some(({ page }) => page.session);
|
|
103
106
|
// Une page réservée demande la session, même si elle ne la lit pas elle-même.
|
|
@@ -433,6 +436,24 @@ ${mouvement ? NETTOYAGE_PRERENDU : ''}};
|
|
|
433
436
|
`;
|
|
434
437
|
}
|
|
435
438
|
/** Disposition.tsx : l'en-tête et le pied communs autour des pages, qui restent montés quand on change de page. */
|
|
439
|
+
/** `src/humalis/aller-au-contenu.tsx` : le premier arrêt du clavier, qui saute l'en-tête. */
|
|
440
|
+
const ALLER_AU_CONTENU = `/** Le lien « Aller au contenu » : caché tant qu'il n'a pas le focus du clavier, il saute l'en-tête. */
|
|
441
|
+
export default function AllerAuContenu() {
|
|
442
|
+
return (
|
|
443
|
+
<a
|
|
444
|
+
className="humalis-aller-au-contenu"
|
|
445
|
+
href="#contenu"
|
|
446
|
+
onClick={(evenement) => {
|
|
447
|
+
const contenu = document.querySelector('main');
|
|
448
|
+
if (!contenu) return;
|
|
449
|
+
evenement.preventDefault();
|
|
450
|
+
contenu.setAttribute('tabindex', '-1');
|
|
451
|
+
contenu.focus();
|
|
452
|
+
}}
|
|
453
|
+
>Aller au contenu</a>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
`;
|
|
436
457
|
function dispositionTsx(d, application) {
|
|
437
458
|
const balise = (genre, composant) => {
|
|
438
459
|
const dispo = application.disposition[genre];
|
|
@@ -441,12 +462,12 @@ function dispositionTsx(d, application) {
|
|
|
441
462
|
return `{${genre} && (\n <${ouvre} className="${classes}">\n <${composant} />\n </${ferme}>\n )}`;
|
|
442
463
|
};
|
|
443
464
|
return `import { Outlet } from 'react-router';
|
|
444
|
-
${d.entete ? `import ${d.entete} from './composants/${d.entete}';\n` : ''}${d.pied ? `import ${d.pied} from './composants/${d.pied}';\n` : ''}
|
|
465
|
+
${d.entete ? `import AllerAuContenu from './humalis/aller-au-contenu';\nimport ${d.entete} from './composants/${d.entete}';\n` : ''}${d.pied ? `import ${d.pied} from './composants/${d.pied}';\n` : ''}
|
|
445
466
|
/** L'en-tête et le pied communs à toutes les pages : montés une fois, ils restent là quand on change de page. */
|
|
446
467
|
export default function Disposition({ entete = true, pied = true }: { entete?: boolean; pied?: boolean }) {
|
|
447
468
|
return (
|
|
448
469
|
<>
|
|
449
|
-
${d.entete ? balise('entete', d.entete) : '{entete && null}'}
|
|
470
|
+
${d.entete ? `{entete && <AllerAuContenu />}\n ${balise('entete', d.entete)}` : '{entete && null}'}
|
|
450
471
|
<Outlet />
|
|
451
472
|
${d.pied ? balise('pied', d.pied) : '{pied && null}'}
|
|
452
473
|
</>
|
|
@@ -512,6 +533,12 @@ ${session ? "import { Session } from './humalis/session';\n" : ''}${partage ? "i
|
|
|
512
533
|
${dispositions.entete || dispositions.pied ? "import Disposition from './Disposition';\n" : ''}${imports}
|
|
513
534
|
import './humalis/styles.css';
|
|
514
535
|
${mouvement ? "import { observerLeDefilement } from './humalis/mouvement';\n" : ''}${feuilles.map((f) => `import './styles/${f}';\n`).join('')}
|
|
536
|
+
// Un hébergeur statique sert /documentation/index.html pour /documentation/ :
|
|
537
|
+
// le routeur lit l'adresse sans « index.html », comme le dossier.
|
|
538
|
+
if (/\\/index\\.html$/.test(location.pathname)) {
|
|
539
|
+
history.replaceState(history.state, '', location.pathname.replace(/index\\.html$/, '') + location.search + location.hash);
|
|
540
|
+
}
|
|
541
|
+
|
|
515
542
|
createRoot(document.getElementById('racine')!).render(
|
|
516
543
|
<StrictMode>
|
|
517
544
|
<BrowserRouter>
|
package/dist/cibles/web/theme.js
CHANGED
|
@@ -102,14 +102,23 @@ export const SCRIPT_THEME = `try{var t=localStorage.getItem('${CLE_THEME}');if(t
|
|
|
102
102
|
/** `src/humalis/theme.ts` du projet web : « changer theme par … ». */
|
|
103
103
|
export function themeTs(defaut) {
|
|
104
104
|
return `/** Le thème choisi par le visiteur (« changer theme par sombre »), gardé dans le navigateur. */
|
|
105
|
-
export type Theme = 'clair' | 'sombre' | 'automatique';
|
|
105
|
+
export type Theme = 'clair' | 'sombre' | 'automatique' | 'bascule';
|
|
106
106
|
|
|
107
107
|
const CLE = '${CLE_THEME}';
|
|
108
108
|
/** Le mode du projet quand le visiteur n'a rien choisi (bloc « theme : »). */
|
|
109
109
|
const PAR_DEFAUT: 'clair' | 'sombre' | undefined = ${defaut ? `'${defaut}'` : 'undefined'};
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
/** Le thème affiché : le choix du visiteur ou du projet, sinon celui du système. */
|
|
112
|
+
function themeAffiche(): 'clair' | 'sombre' {
|
|
113
|
+
const choisi = document.documentElement.dataset.theme;
|
|
114
|
+
if (choisi === 'clair' || choisi === 'sombre') return choisi;
|
|
115
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'sombre' : 'clair';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function definirTheme(demande: Theme): void {
|
|
112
119
|
const racine = document.documentElement;
|
|
120
|
+
// « basculer theme » : l'inverse de ce qu'on voit.
|
|
121
|
+
const theme = demande === 'bascule' ? (themeAffiche() === 'sombre' ? 'clair' : 'sombre') : demande;
|
|
113
122
|
try {
|
|
114
123
|
if (theme === 'automatique') localStorage.removeItem(CLE);
|
|
115
124
|
else localStorage.setItem(CLE, theme);
|
|
@@ -38,6 +38,7 @@ export function typeDe(valeur, contexte) {
|
|
|
38
38
|
case 'utilisateur':
|
|
39
39
|
case 'etatRequete':
|
|
40
40
|
case 'mouvementReduit':
|
|
41
|
+
case 'themeSombre':
|
|
41
42
|
return { genre: 'booleen' };
|
|
42
43
|
case 'erreurRequete':
|
|
43
44
|
return { genre: 'texte' };
|
|
@@ -150,6 +151,9 @@ export function expression(valeur, contexte) {
|
|
|
150
151
|
case 'mouvementReduit':
|
|
151
152
|
// « réduire les animations » du système, suivi en direct (src/humalis/temps.ts).
|
|
152
153
|
return 'mouvementReduit';
|
|
154
|
+
case 'themeSombre':
|
|
155
|
+
// Le thème affiché, suivi en direct (src/humalis/temps.ts).
|
|
156
|
+
return 'themeSombre';
|
|
153
157
|
case 'etatRequete':
|
|
154
158
|
// L'état de la requête de la variable (src/humalis/serveur.ts) : chargement, erreur ou introuvable.
|
|
155
159
|
return `${camel(valeur.variable)}Req.statut === '${valeur.etat}'`;
|
|
@@ -187,6 +191,19 @@ function texteTS(parties, contexte, langue) {
|
|
|
187
191
|
}
|
|
188
192
|
/** Le contenu d'un élément dans du JSX : `Bienvenue`, `{nom}`, `{panier.join(', ')}`. */
|
|
189
193
|
export function contenuJSX(valeur, contexte) {
|
|
194
|
+
if (valeur.type === 'texte' && (valeur.parties.some((p) => 'texte' in p && (p.accent || p.code)) || valeur.versions?.some((v) => v.parties.some((p) => 'texte' in p && (p.accent || p.code))))) {
|
|
195
|
+
const enfants = (parties) => parties.map((p) => {
|
|
196
|
+
if ('texte' in p)
|
|
197
|
+
return p.accent
|
|
198
|
+
? `<span className="humalis-accent">{${chaine(p.texte)}}</span>`
|
|
199
|
+
: p.code ? `<code className="humalis-code-ligne">{${chaine(p.texte)}}</code>`
|
|
200
|
+
: `{${chaine(p.texte)}}`;
|
|
201
|
+
return contenuJSX({ type: 'texte', parties: [p] }, contexte);
|
|
202
|
+
}).join('');
|
|
203
|
+
if (valeur.versions)
|
|
204
|
+
return valeur.versions.map((v) => `{langue === ${chaine(v.code)} && <>${enfants(v.parties)}</>}`).join('');
|
|
205
|
+
return enfants(valeur.parties);
|
|
206
|
+
}
|
|
190
207
|
if (valeur.type === 'texte' && !valeur.versions && valeur.parties.every((p) => 'texte' in p)) {
|
|
191
208
|
const texte = valeur.parties.map((p) => ('texte' in p ? p.texte : '')).join('');
|
|
192
209
|
// Les caractères qui ont un sens en JSX passent par une chaîne.
|
package/dist/i18n/messages.js
CHANGED
|
@@ -65,8 +65,10 @@ export const MESSAGES = [
|
|
|
65
65
|
{ fr: "Choisissez d'abord un fichier.", en: 'Choose a file first.' },
|
|
66
66
|
{ fr: 'AAAA-MM-JJ', en: 'YYYY-MM-DD' },
|
|
67
67
|
{ fr: 'Fermer le message', en: 'Close the message' },
|
|
68
|
+
{ fr: 'Aller au contenu', en: 'Skip to content' },
|
|
68
69
|
{ fr: 'Fermer', en: 'Close' },
|
|
69
70
|
{ fr: 'Copié', en: 'Copied' },
|
|
71
|
+
{ fr: 'Copier', en: 'Copy' },
|
|
70
72
|
{ fr: 'Copie impossible.', en: 'Could not copy.' },
|
|
71
73
|
{ fr: 'Lien copié : il ne reste qu’à le coller.', en: 'Link copied: paste it wherever you like.' },
|
|
72
74
|
{ fr: 'Texte copié : il ne reste qu’à le coller.', en: 'Text copied: paste it wherever you like.' },
|