@mostajs/wagon-ui 0.4.0 → 0.5.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@
5
5
 
6
6
 
7
7
 
8
+ ## 0.5.1 — 2026-10-09
9
+
10
+ **Corrigé** (audit du 09/10/2026) : `page({ chemin })` — l'adresse canonique et les liens de langue désignent la page
11
+ affichée, pas l'onglet actif (une fiche /voyages/<circuit> déclarait /voyages ; changer de langue ramenait à la
12
+ liste). Sans `chemin`, rien ne change. Essai T-WUI-22.
13
+
14
+ ## 0.5.0 — 2026-10-08
15
+
16
+ L'en-tête de la maquette Bolt de travel, devenu disponible pour tous les sites du wagon. Trois options, toutes
17
+ désactivées par défaut : un site qui ne les demande pas reste identique.
18
+
19
+ **Ajouté**
20
+ - `collant: true` : l'en-tête reste en haut de l'écran au défilement (`position: sticky`).
21
+ - `appel: { href, libelle: { fr, en, ar } }` : un bouton d'action dans l'en-tête (« Demander un devis »).
22
+ - `selecteurLangue: 'menu'` : la langue se choisit dans un menu déroulant (`<details>`, donc sans script),
23
+ langue courante marquée par `aria-current` ; sur mobile, le menu se déplie dans le hamburger.
24
+
25
+ **Essais** : T-WUI-19 à 21.
26
+
8
27
  ## 0.4.0 — 2026-10-08
9
28
 
10
29
  - **Les textes passent par `@mostajs/i18n`** (≥ 0.2.0) au lieu d'un traducteur maison : même ordre de résolution
package/llms.txt CHANGED
@@ -26,6 +26,8 @@ Rendre les écrans communs à tout SaaS, identiques d'un produit à l'autre, san
26
26
  `/`, `/tarifs`, `/aide`, `/apropos`, `/inscription` (POST : plan, nom, courriel, motdepasse), `/connexion` (POST : courriel, motdepasse), `/deconnexion`, `/compte`, `/compte/cles` (POST : libelle), `/compte/cles/revoquer` (POST : prefix), `/compte/portail` (POST), `/paiement` (POST : compte, plan), `/admin/plans` (POST), `/admin/usage`, `/mentions-legales`, `/cgu`, `/cgv`, `/confidentialite`.
27
27
 
28
28
  ## PIÈGES
29
+ - 0.5.1 : `page({ chemin })` — canonique et liens de langue sur la page réelle.
30
+ - 0.5.0 : `creerWagonUi({ …, collant: true, appel: { href, libelle }, selecteurLangue: 'menu' })` — en-tête collant, bouton d'appel, langue en menu déroulant sans script ; défauts inchangés.
29
31
  - 0.4.0 : `tarifs.clesPluriel` n'existe plus → `tarifs.cles_one` / `tarifs.cles_other` (+ formes arabes) ; prix formatés par langue (« 9,99 € »).
30
32
  - `legal({ html })` n'échappe pas `html` : il doit venir des textes de l'éditeur (mostajs/templates), jamais d'un formulaire.
31
33
  - Les prix sont en CENTIMES. `-1` = sans limite ; `0` = non compris.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/wagon-ui",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Les pages du wagon d'un SaaS — mise en page (langues, pied légal, en-têtes pour les moteurs), accueil, tarifs, inscription, connexion (mot de passe ou code), espace client (palier, jauges, clés, factures, gestion de l'abonnement), administration (paliers, usage), pages légales — en fr, en, ar. Extraites de mdseal, sans ce qu'il vend : l'identité et les ressources mesurées viennent de la configuration. Vues pures, zéro dépendance.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
package/src/index.js CHANGED
@@ -116,7 +116,18 @@ header.bande{max-width:none;background:var(--brand);border-bottom:0;padding:.8re
116
116
  header.bande .brand,header.bande nav a,header.bande .moi a,header.bande .langues a,header.bande .langues b,header.bande .langues{color:var(--text-on-brand)}
117
117
  header.bande nav a{opacity:.9} header.bande nav a.on,header.bande nav a:hover{background:rgba(255,255,255,.16);opacity:1}
118
118
  header.bande .burger{color:var(--text-on-brand);border-color:rgba(255,255,255,.4)}
119
- header.bande + main{padding-top:1.2rem}`;
119
+ header.bande + main{padding-top:1.2rem}
120
+ header.collant{position:sticky;top:0;z-index:50;box-shadow:0 6px 20px -14px rgba(15,23,42,.45)}
121
+ header .appel{display:inline-block;padding:.45em 1.1em;border-radius:99px;background:var(--surface);color:var(--brand);font-weight:700;text-decoration:none;white-space:nowrap;border:1px solid var(--line)}
122
+ header.bande .appel{background:var(--surface);color:var(--brand);border-color:transparent}
123
+ .choix-langue{position:relative;font-size:.9em}
124
+ .choix-langue summary{list-style:none;cursor:pointer;padding:.35em .8em;border-radius:99px;border:1px solid var(--line);color:var(--link);user-select:none;white-space:nowrap}
125
+ .choix-langue summary::-webkit-details-marker{display:none;}
126
+ header.bande .choix-langue summary{color:var(--text-on-brand);border-color:rgba(255,255,255,.35);background:rgba(255,255,255,.1)}
127
+ .choix-langue ul{position:absolute;inset-inline-end:0;top:calc(100% + .4em);margin:0;padding:.3em 0;list-style:none;min-width:10em;background:var(--surface);border:1px solid var(--line);border-radius:12px;box-shadow:0 18px 35px -18px rgba(15,23,42,.45);z-index:60}
128
+ .choix-langue li a{display:block;padding:.45em 1em;color:var(--ink,var(--text));text-decoration:none}
129
+ .choix-langue li a:hover{background:var(--fond-doux)} .choix-langue li a[aria-current]{font-weight:700;color:var(--brand)}
130
+ @media (max-width: 760px){ .choix-langue ul{position:static;box-shadow:none;margin-top:.3em} header .appel{text-align:center} }`;
120
131
  }
121
132
 
122
133
  /** Un thème de Palette Studio n'est accepté que s'il ne fait que redéfinir des jetons dans :root{…}. */
@@ -161,7 +172,7 @@ const DOCUMENTS_LEGAUX = Object.freeze([['mentions-legales', 'mentions'], ['cgu'
161
172
  * parPeriode?: string|object, sansLimite?: string|object, aucun?: string|object }>} [o.ressources]
162
173
  * @param {object} [o.textes] surcharges des libellés, par langue : { fr: { tarifs: { note: '…' } } }
163
174
  */
164
- export function creerWagonUi({ marque, couleurs = {}, langues = ['fr', 'en', 'ar'], liens = [], ressources = [], textes = {}, theme = null, entete = 'simple' } = {}) {
175
+ export function creerWagonUi({ marque, couleurs = {}, langues = ['fr', 'en', 'ar'], liens = [], ressources = [], textes = {}, theme = null, entete = 'simple', collant = false, appel = null, selecteurLangue = 'liens' } = {}) {
165
176
  if (!String(marque?.nom ?? '').trim()) throw new Error('@mostajs/wagon-ui : `marque.nom` requis — le nom du service');
166
177
  if (!String(marque?.auteur ?? '').trim()) throw new Error('@mostajs/wagon-ui : `marque.auteur` requis — qui édite le service');
167
178
  const LANGUES = langues.filter((l) => CATALOGUES[l]);
@@ -211,24 +222,33 @@ export function creerWagonUi({ marque, couleurs = {}, langues = ['fr', 'en', 'ar
211
222
  * La page entière. `contenu` est du HTML déjà rendu (par une vue de ce module, ou par la charge
212
223
  * utile). `base` — l'adresse publique — ajoute les en-têtes que lisent les moteurs et les aperçus.
213
224
  */
214
- page({ actif = '/', titre = '', contenu = '', lang, connecte = false, base = '', description = '' }) {
225
+ page({ actif = '/', chemin: cheminPage = '', titre = '', contenu = '', lang, connecte = false, base = '', description = '' }) {
215
226
  const L = langueDe(lang); const tr = t(L);
216
227
  const nom = esc(marque.nom);
217
228
  const slogan = dansLangue(marque.slogan, L);
218
- const chemin = lien(actif);
219
- const canonique = base ? `${String(base).replace(/\/$/, '')}${actif === '/' ? '/' : actif}` : '';
229
+ // 0.5.1 — la page RÉELLE (`chemin`) fait l'adresse canonique et les liens de langue ; l'onglet (`actif`) ne
230
+ // sert qu'à la navigation. Sans `chemin`, comportement d'avant.
231
+ const ici = cheminPage || actif;
232
+ const chemin = lien(ici);
233
+ const canonique = base ? `${String(base).replace(/\/$/, '')}${ici === '/' ? '/' : ici}` : '';
220
234
  const tete = canonique ? `<meta name="description" content="${esc(description || slogan)}">
221
235
  <link rel="canonical" href="${esc(canonique)}">${LANGUES.map((l) => `<link rel="alternate" hreflang="${l}" href="${esc(canonique)}?lang=${l}">`).join('')}<link rel="alternate" hreflang="x-default" href="${esc(canonique)}">
222
236
  <meta property="og:type" content="website"><meta property="og:site_name" content="${nom}"><meta property="og:title" content="${esc(titre)} — ${nom}">
223
237
  <meta property="og:description" content="${esc(description || slogan)}"><meta property="og:url" content="${esc(canonique)}"><meta property="og:locale" content="${L}">` : '';
224
238
  const nav = [...liens.map((x) => [x.href, dansLangue(x.libelle, L)]), ['/tarifs', tr('nav.tarifs')], ['/aide', tr('nav.aide')], ['/apropos', tr('nav.apropos')]];
225
- const choixLangue = LANGUES.length < 2 ? '' : `<span class="langues">${LANGUES.map((l) => (l === L
226
- ? `<b lang="${l}">${esc(LIBELLES_LANGUES[l])}</b>`
227
- : `<a href="${chemin}?lang=${l}" hreflang="${l}" lang="${l}" rel="alternate">${esc(LIBELLES_LANGUES[l])}</a>`)).join(' · ')}</span>`;
239
+ // 0.5 — le choix de langue en menu déroulant (`selecteurLangue: 'menu'`) : <details>, donc SANS script.
240
+ const choixLangue = LANGUES.length < 2 ? '' : selecteurLangue === 'menu'
241
+ ? `<details class="choix-langue"><summary aria-label="${esc(tr('layout.langue'))}">🌐 ${L.toUpperCase()} ▾</summary><ul>${LANGUES.map((l) => `<li><a href="${chemin}?lang=${l}" hreflang="${l}" lang="${l}" rel="alternate"${l === L ? ' aria-current="true"' : ''}>${esc(LIBELLES_LANGUES[l])}</a></li>`).join('')}</ul></details>`
242
+ : `<span class="langues">${LANGUES.map((l) => (l === L
243
+ ? `<b lang="${l}">${esc(LIBELLES_LANGUES[l])}</b>`
244
+ : `<a href="${chemin}?lang=${l}" hreflang="${l}" lang="${l}" rel="alternate">${esc(LIBELLES_LANGUES[l])}</a>`)).join(' · ')}</span>`;
245
+ // 0.5 — l'en-tête collant (`collant`) et le bouton d'appel (`appel: { href, libelle }`).
246
+ const classesTete = [entete === 'barre' ? 'bande' : '', collant ? 'collant' : ''].filter(Boolean).join(' ');
247
+ const boutonAppel = appel?.href ? `<a class="appel" href="${lien(appel.href)}">${esc(dansLangue(appel.libelle, L))}</a>` : '';
228
248
  return `<!doctype html><html lang="${L}" dir="${dirDe(L)}"><head><meta charset="utf-8">
229
249
  <meta name="viewport" content="width=device-width, initial-scale=1"><title>${esc(titre)} — ${nom}</title>
230
250
  ${tete}<style>${STYLE}</style>${themeCss() ? `<style id="theme-palette">${themeCss()}</style>` : ''}</head><body>
231
- <header${entete === 'barre' ? ' class="bande"' : ''}><a class="brand" href="/">${marque.symbole ? `${esc(marque.symbole)} ` : ''}${nom}</a><input type="checkbox" id="wagon-menu" class="bascule"><label for="wagon-menu" class="burger" aria-label="${esc(tr('layout.menu'))}">☰</label><div class="menu"><nav>${nav.map(([h, l]) => `<a href="${lien(h)}"${actif === h ? ' class="on"' : ''}>${esc(l)}</a>`).join('')}</nav><span class="moi"><a href="${connecte ? '/compte' : '/connexion'}">${esc(tr(connecte ? 'espace.compte' : 'espace.seConnecter'))}</a>${connecte ? ` · <a href="/deconnexion">${esc(tr('espace.seDeconnecter'))}</a>` : ''}</span>${choixLangue}</div></header>
251
+ <header${classesTete ? ` class="${classesTete}"` : ''}><a class="brand" href="/">${marque.symbole ? `${esc(marque.symbole)} ` : ''}${nom}</a><input type="checkbox" id="wagon-menu" class="bascule"><label for="wagon-menu" class="burger" aria-label="${esc(tr('layout.menu'))}">☰</label><div class="menu"><nav>${nav.map(([h, l]) => `<a href="${lien(h)}"${actif === h ? ' class="on"' : ''}>${esc(l)}</a>`).join('')}</nav><span class="moi"><a href="${connecte ? '/compte' : '/connexion'}">${esc(tr(connecte ? 'espace.compte' : 'espace.seConnecter'))}</a>${connecte ? ` · <a href="/deconnexion">${esc(tr('espace.seDeconnecter'))}</a>` : ''}</span>${boutonAppel}${choixLangue}</div></header>
232
252
  <main>${contenu}</main>
233
253
  <footer><b>${nom}</b>${slogan ? ` — ${esc(slogan)}` : ''} · ${esc(tr('layout.auteur'))} : <b>${esc(marque.auteur)}</b>${marque.courriel ? ` &lt;${esc(marque.courriel)}&gt;` : ''} · ${esc(marque.licence ?? 'AGPL-3.0')} · ${esc(tr('layout.propulse'))} <code>@mostajs/*</code><br><span class="legal">${DOCUMENTS_LEGAUX.map(([h, k]) => `<a href="/${h}">${esc(tr(`legal.${k}`))}</a>`).join(' · ')}</span></footer>
234
254
  </body></html>`;