@starterlib/ui 0.3.0 → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, LOCALE_ID, signal, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, ElementRef, DestroyRef, output, model, afterNextRender, effect, TemplateRef, Directive, contentChild, linkedSignal, isDevMode } from '@angular/core';
3
- import { masquerIban, masquerTelephone, masquerCarte, quatreDerniers, formaterIban, formaterMontant, CHOIX_DE_THEME, LANGUES } from '@starterlib/util';
2
+ import { InjectionToken, inject, LOCALE_ID, signal, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, ElementRef, DestroyRef, output, model, afterNextRender, viewChild, Directive, effect, Injector, linkedSignal, TemplateRef, contentChild, isDevMode } from '@angular/core';
3
+ import { masquerIban, masquerTelephone, masquerCarte, quatreDerniers, formaterIban, formaterMontant, formaterTaille, formaterDateLongue, montant, decimalesDe, motifDeDate, formaterDateSaisie, ibanEstValide, normaliserTelephone, typeDeFichierAccepte, normaliserIban, bornerDate, premierJourDeLaSemaine, ajouterJours, semainesDuMois, ajouterMois, jourDeLaSemaine, CHOIX_DE_THEME, LANGUES } from '@starterlib/util';
4
4
  export { ETATS_DE_BLOC } from '@starterlib/util';
5
5
  import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
6
6
  import { RouterLink } from '@angular/router';
@@ -56,7 +56,66 @@ const LIBELLES_FR = {
56
56
  sessionExpireDans: (duree) => `Sans activit\u00e9 de votre part, la session sera ferm\u00e9e dans ${duree}.`,
57
57
  resterConnecte: 'Rester connect\u00e9',
58
58
  seDeconnecter: 'Se d\u00e9connecter',
59
+ obligatoire: '(obligatoire)',
60
+ champObligatoire: 'Ce champ est obligatoire.',
61
+ valeurInvalide: 'Cette valeur n\u2019est pas valide.',
62
+ adresseInvalide: 'Saisissez une adresse \u00e9lectronique, par exemple nom@exemple.fr.',
63
+ formatInvalide: 'Le format n\u2019est pas valide.',
64
+ longueurMinimale: (minimum) => `Saisissez au moins ${String(minimum)} caract\u00e8res.`,
65
+ longueurMaximale: (maximum) => `Saisissez au plus ${String(maximum)} caract\u00e8res.`,
66
+ valeurMinimale: (minimum) => `La valeur doit \u00eatre au moins ${minimum}.`,
67
+ valeurMaximale: (maximum) => `La valeur doit \u00eatre au plus ${maximum}.`,
68
+ caracteresRestants: (restants) => restants <= 1
69
+ ? `${String(restants)} caract\u00e8re restant`
70
+ : `${String(restants)} caract\u00e8res restants`,
71
+ caracteresEnTrop: (enTrop) => enTrop === 1 ? '1 caract\u00e8re en trop' : `${String(enTrop)} caract\u00e8res en trop`,
72
+ erreursACorriger: (nombre) => nombre === 1 ? 'Une erreur \u00e0 corriger' : `${String(nombre)} erreurs \u00e0 corriger`,
73
+ afficherLeMotDePasse: 'Afficher le mot de passe',
74
+ codeAChiffres: (longueur) => `Code \u00e0 ${String(longueur)} chiffres.`,
75
+ ibanInvalide: 'Saisissez un IBAN valide.',
76
+ telephoneInvalide: 'Saisissez un num\u00e9ro de t\u00e9l\u00e9phone en chiffres, avec l\u2019indicatif du pays pour un num\u00e9ro \u00e9tranger.',
77
+ exempleDIban: 'Par exemple FR76 3000 6000 0112 3456 7890 189.',
78
+ rechercher: 'Rechercher',
59
79
  fermerLeMessage: 'Fermer le message',
80
+ montantIllisible: (exemple) => `Saisissez un montant en chiffres, par exemple ${exemple}.`,
81
+ tropDeDecimales: (decimales) => decimales === 0
82
+ ? 'Saisissez un montant sans d\u00e9cimales.'
83
+ : `Saisissez au plus ${String(decimales)} d\u00e9cimale${decimales > 1 ? 's' : ''}.`,
84
+ deviseDuMontant: (devise) => `Devise\u00a0: ${devise}.`,
85
+ lettreDuJour: 'jj',
86
+ lettreDuMois: 'mm',
87
+ lettreDeLAnnee: 'aaaa',
88
+ formatDeDate: (motif, exemple) => `Au format ${motif}, par exemple ${exemple}.`,
89
+ dateIllisible: (motif, exemple) => `Saisissez une date qui existe, au format ${motif}, par exemple ${exemple}.`,
90
+ dateTropTot: (date) => `Saisissez une date \u00e0 partir du ${date}.`,
91
+ dateTropTard: (date) => `Saisissez une date jusqu\u2019au ${date}.`,
92
+ finAvantDebut: 'La date de fin ne peut pas pr\u00e9c\u00e9der la date de d\u00e9but.',
93
+ dateDeDebut: 'Date de d\u00e9but',
94
+ dateDeFin: 'Date de fin',
95
+ choisirDansLeCalendrier: (champ) => `${champ}\u00a0: choisir dans le calendrier`,
96
+ moisPrecedent: 'Mois pr\u00e9c\u00e9dent',
97
+ moisSuivant: 'Mois suivant',
98
+ aideDuCalendrier: 'Fl\u00e8ches pour changer de jour, Page pr\u00e9c\u00e9dente et Page suivante pour changer de mois, \u00c9chap pour fermer.',
99
+ choisir: 'Choisir\u2026',
100
+ nombreDeResultats: (nombre) => nombre === 0
101
+ ? 'Aucun r\u00e9sultat'
102
+ : `${String(nombre)} r\u00e9sultat${nombre > 1 ? 's' : ''}`,
103
+ nombreDeSelections: (nombre) => (nombre === 0 ? 'Aucun choix' : `${String(nombre)} choix`),
104
+ retirerLeChoix: (choix) => `Retirer ${choix}`,
105
+ afficherLesChoix: (champ) => `${champ}\u00a0: afficher les choix`,
106
+ choisirDesFichiers: 'Choisir des fichiers',
107
+ choisirUnFichier: 'Choisir un fichier',
108
+ ouDeposerIci: 'ou d\u00e9posez-les ici',
109
+ aucunFichier: 'Aucun fichier choisi.',
110
+ nombreDeFichiers: (nombre) => nombre === 0
111
+ ? 'Aucun fichier choisi.'
112
+ : `${String(nombre)} fichier${nombre > 1 ? 's' : ''} choisi${nombre > 1 ? 's' : ''}.`,
113
+ retirerLeFichier: (fichier) => `Retirer ${fichier}`,
114
+ fichierTropVolumineux: (fichier, taille) => `${fichier} d\u00e9passe ${taille}.`,
115
+ typeDeFichierRefuse: (fichier) => `${fichier} n\u2019est pas d\u2019un type accept\u00e9.`,
116
+ tropDeFichiers: (nombre) => `Choisissez au plus ${String(nombre)} fichier${nombre > 1 ? 's' : ''}.`,
117
+ typesAcceptes: (types) => `Types accept\u00e9s\u00a0: ${types}.`,
118
+ tailleMaximale: (taille) => `${taille} au plus par fichier.`,
60
119
  confirmer: 'Confirmer',
61
120
  annuler: 'Annuler',
62
121
  recopierPourConfirmer: (texte) => `Pour confirmer, recopiez \u00ab\u202f${texte}\u202f\u00bb`,
@@ -97,7 +156,60 @@ const LIBELLES_EN = {
97
156
  sessionExpireDans: (duree) => `Without any activity, you will be signed out in ${duree}.`,
98
157
  resterConnecte: 'Stay signed in',
99
158
  seDeconnecter: 'Sign out',
159
+ obligatoire: '(required)',
160
+ champObligatoire: 'This field is required.',
161
+ valeurInvalide: 'This value is not valid.',
162
+ adresseInvalide: 'Enter an email address, like name@example.com.',
163
+ formatInvalide: 'The format is not valid.',
164
+ longueurMinimale: (minimum) => `Enter at least ${String(minimum)} characters.`,
165
+ longueurMaximale: (maximum) => `Enter no more than ${String(maximum)} characters.`,
166
+ valeurMinimale: (minimum) => `The value must be at least ${minimum}.`,
167
+ valeurMaximale: (maximum) => `The value must be at most ${maximum}.`,
168
+ caracteresRestants: (restants) => restants === 1 ? '1 character remaining' : `${String(restants)} characters remaining`,
169
+ caracteresEnTrop: (enTrop) => enTrop === 1 ? '1 character too many' : `${String(enTrop)} characters too many`,
170
+ erreursACorriger: (nombre) => nombre === 1 ? 'There is 1 problem to fix' : `There are ${String(nombre)} problems to fix`,
171
+ afficherLeMotDePasse: 'Show password',
172
+ codeAChiffres: (longueur) => `${String(longueur)}-digit code.`,
173
+ ibanInvalide: 'Enter a valid IBAN.',
174
+ telephoneInvalide: 'Enter a phone number using digits, with the country code for a number abroad.',
175
+ exempleDIban: 'For example FR76 3000 6000 0112 3456 7890 189.',
176
+ rechercher: 'Search',
100
177
  fermerLeMessage: 'Dismiss message',
178
+ montantIllisible: (exemple) => `Enter an amount in figures, like ${exemple}.`,
179
+ tropDeDecimales: (decimales) => decimales === 0
180
+ ? 'Enter an amount without decimals.'
181
+ : `Enter no more than ${String(decimales)} decimal place${decimales > 1 ? 's' : ''}.`,
182
+ deviseDuMontant: (devise) => `Currency: ${devise}.`,
183
+ lettreDuJour: 'dd',
184
+ lettreDuMois: 'mm',
185
+ lettreDeLAnnee: 'yyyy',
186
+ formatDeDate: (motif, exemple) => `Use the format ${motif}, like ${exemple}.`,
187
+ dateIllisible: (motif, exemple) => `Enter a real date in the format ${motif}, like ${exemple}.`,
188
+ dateTropTot: (date) => `Enter a date on or after ${date}.`,
189
+ dateTropTard: (date) => `Enter a date on or before ${date}.`,
190
+ finAvantDebut: 'The end date cannot be before the start date.',
191
+ dateDeDebut: 'Start date',
192
+ dateDeFin: 'End date',
193
+ choisirDansLeCalendrier: (champ) => `${champ}: choose from calendar`,
194
+ moisPrecedent: 'Previous month',
195
+ moisSuivant: 'Next month',
196
+ aideDuCalendrier: 'Arrow keys to change day, Page Up and Page Down to change month, Escape to close.',
197
+ choisir: 'Choose\u2026',
198
+ nombreDeResultats: (nombre) => nombre === 0 ? 'No results' : `${String(nombre)} result${nombre > 1 ? 's' : ''}`,
199
+ nombreDeSelections: (nombre) => nombre === 0 ? 'Nothing selected' : `${String(nombre)} selected`,
200
+ retirerLeChoix: (choix) => `Remove ${choix}`,
201
+ afficherLesChoix: (champ) => `${champ}: show options`,
202
+ choisirDesFichiers: 'Choose files',
203
+ choisirUnFichier: 'Choose a file',
204
+ ouDeposerIci: 'or drop them here',
205
+ aucunFichier: 'No file chosen.',
206
+ nombreDeFichiers: (nombre) => nombre === 0 ? 'No file chosen.' : `${String(nombre)} file${nombre > 1 ? 's' : ''} chosen.`,
207
+ retirerLeFichier: (fichier) => `Remove ${fichier}`,
208
+ fichierTropVolumineux: (fichier, taille) => `${fichier} is larger than ${taille}.`,
209
+ typeDeFichierRefuse: (fichier) => `${fichier} is not an accepted file type.`,
210
+ tropDeFichiers: (nombre) => `Choose no more than ${String(nombre)} file${nombre > 1 ? 's' : ''}.`,
211
+ typesAcceptes: (types) => `Accepted types: ${types}.`,
212
+ tailleMaximale: (taille) => `${taille} maximum per file.`,
101
213
  confirmer: 'Confirm',
102
214
  annuler: 'Cancel',
103
215
  recopierPourConfirmer: (texte) => `To confirm, type \u201c${texte}\u201d`,
@@ -371,6 +483,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
371
483
  */
372
484
  const NOMS_DICONE = [
373
485
  'boite-vide',
486
+ 'calendrier',
374
487
  'cercle',
375
488
  'cercle-coche',
376
489
  'cercle-croix',
@@ -383,6 +496,7 @@ const NOMS_DICONE = [
383
496
  'copie',
384
497
  'corbeille',
385
498
  'croix',
499
+ 'fichier',
386
500
  'hors-ligne',
387
501
  'lien-externe',
388
502
  'menu',
@@ -392,6 +506,7 @@ const NOMS_DICONE = [
392
506
  'rafraichir',
393
507
  'recherche',
394
508
  'serveur',
509
+ 'televerser',
395
510
  'triangle-alerte',
396
511
  'utilisateur',
397
512
  ];
@@ -2094,6 +2209,1578 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
2094
2209
  }, template: "<st-icon [nom]=\"icone()\" taille=\"xs\" />\n<span class=\"st-inline-message__texte\"><ng-content /></span>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:flex;gap:var(--st-espacement-2);align-items:flex-start;font-size:var(--st-taille-sm);line-height:var(--st-interligne-normal)}:host([data-ton=neutre]){color:var(--st-couleur-texte-discret)}:host([data-ton=primaire]){color:var(--st-couleur-primaire)}:host([data-ton=succes]){color:var(--st-couleur-succes)}:host([data-ton=avertissement]){color:var(--st-couleur-avertissement)}:host([data-ton=danger]){color:var(--st-couleur-danger)}\n"] }]
2095
2210
  }], propDecorators: { ton: [{ type: i0.Input, args: [{ isSignal: true, alias: "ton", required: false }] }], identifiant: [{ type: i0.Input, args: [{ isSignal: true, alias: "identifiant", required: false }] }] } });
2096
2211
 
2212
+ /**
2213
+ * Le texte d'une erreur, dans la langue courante.
2214
+ *
2215
+ * ⚠️ **Un `message` fourni l'emporte toujours.** Un validateur metier, ou une
2216
+ * erreur du serveur deja traduite, en sait plus que le genre : « IBAN d'un pays
2217
+ * non pris en charge » vaut mieux que « Le format n'est pas valide ».
2218
+ *
2219
+ * ⚠️ **Un genre inconnu rend un message generique, jamais le genre brut.**
2220
+ * Afficher `ibanChecksum` a une personne qui saisit un IBAN, c'est lui montrer le
2221
+ * code source.
2222
+ *
2223
+ * La locale sert aux messages qui citent un format, une date ou une taille : « au
2224
+ * format jj/mm/aaaa » n'est pas « mm/dd/yyyy ».
2225
+ */
2226
+ function messageDErreur(erreur, libelles, locale = 'fr-FR') {
2227
+ const fourni = erreur.message?.trim();
2228
+ if (fourni !== undefined && fourni !== '')
2229
+ return fourni;
2230
+ const propriete = (nom) => erreur[nom];
2231
+ const nombre = (nom) => {
2232
+ const valeur = propriete(nom);
2233
+ return typeof valeur === 'number' ? valeur : undefined;
2234
+ };
2235
+ const texte = (nom) => {
2236
+ const valeur = propriete(nom);
2237
+ return typeof valeur === 'string' ? valeur : undefined;
2238
+ };
2239
+ switch (erreur.kind) {
2240
+ case 'required':
2241
+ return libelles.champObligatoire;
2242
+ case 'email':
2243
+ return libelles.adresseInvalide;
2244
+ case 'pattern':
2245
+ return libelles.formatInvalide;
2246
+ case 'iban':
2247
+ return libelles.ibanInvalide;
2248
+ case 'telephone':
2249
+ return libelles.telephoneInvalide;
2250
+ case 'minLength': {
2251
+ const minimum = nombre('minLength');
2252
+ return minimum === undefined ? libelles.valeurInvalide : libelles.longueurMinimale(minimum);
2253
+ }
2254
+ case 'maxLength': {
2255
+ const maximum = nombre('maxLength');
2256
+ return maximum === undefined ? libelles.valeurInvalide : libelles.longueurMaximale(maximum);
2257
+ }
2258
+ case 'min': {
2259
+ const minimum = nombre('min');
2260
+ return minimum === undefined
2261
+ ? libelles.valeurInvalide
2262
+ : libelles.valeurMinimale(String(minimum));
2263
+ }
2264
+ case 'max': {
2265
+ const maximum = nombre('max');
2266
+ return maximum === undefined
2267
+ ? libelles.valeurInvalide
2268
+ : libelles.valeurMaximale(String(maximum));
2269
+ }
2270
+ case 'montant': {
2271
+ const devise = texte('devise') ?? 'EUR';
2272
+ const exemple = formaterMontant(montant((123456n * 10n ** BigInt(decimalesDe(devise))) / 100n, devise), {
2273
+ locale,
2274
+ sansDevise: true,
2275
+ });
2276
+ return libelles.montantIllisible(exemple);
2277
+ }
2278
+ case 'decimales': {
2279
+ const decimales = nombre('decimales');
2280
+ return decimales === undefined
2281
+ ? libelles.valeurInvalide
2282
+ : libelles.tropDeDecimales(decimales);
2283
+ }
2284
+ case 'date':
2285
+ return libelles.dateIllisible(motifLocal(libelles, locale), exempleDeDate(locale));
2286
+ case 'dateMin': {
2287
+ const minimum = texte('min');
2288
+ return minimum === undefined
2289
+ ? libelles.valeurInvalide
2290
+ : libelles.dateTropTot(formaterDateLongue(minimum, locale));
2291
+ }
2292
+ case 'dateMax': {
2293
+ const maximum = texte('max');
2294
+ return maximum === undefined
2295
+ ? libelles.valeurInvalide
2296
+ : libelles.dateTropTard(formaterDateLongue(maximum, locale));
2297
+ }
2298
+ case 'plageDeDates':
2299
+ return libelles.finAvantDebut;
2300
+ case 'fichierTaille': {
2301
+ const fichier = texte('fichier');
2302
+ const maximum = nombre('max');
2303
+ return fichier === undefined || maximum === undefined
2304
+ ? libelles.valeurInvalide
2305
+ : libelles.fichierTropVolumineux(fichier, formaterTaille(maximum, locale));
2306
+ }
2307
+ case 'fichierType': {
2308
+ const fichier = texte('fichier');
2309
+ return fichier === undefined
2310
+ ? libelles.valeurInvalide
2311
+ : libelles.typeDeFichierRefuse(fichier);
2312
+ }
2313
+ case 'fichiersNombre': {
2314
+ const maximum = nombre('max');
2315
+ return maximum === undefined ? libelles.valeurInvalide : libelles.tropDeFichiers(maximum);
2316
+ }
2317
+ default:
2318
+ return libelles.valeurInvalide;
2319
+ }
2320
+ }
2321
+ /** Le motif a taper dans la langue courante : `jj/mm/aaaa`. */
2322
+ function motifLocal(libelles, locale) {
2323
+ return motifDeDate(locale, {
2324
+ jour: libelles.lettreDuJour,
2325
+ mois: libelles.lettreDuMois,
2326
+ annee: libelles.lettreDeLAnnee,
2327
+ });
2328
+ }
2329
+ /**
2330
+ * Un jour superieur a 12 : « 31/03/2026 » montre sans ambiguite ou va le jour,
2331
+ * ce que « 01/02/2026 » ne dit pas.
2332
+ */
2333
+ function exempleDeDate(locale) {
2334
+ return formaterDateSaisie('2026-03-31', locale);
2335
+ }
2336
+ /**
2337
+ * Erreur d'IBAN, a brancher sur un schema :
2338
+ *
2339
+ * ```ts
2340
+ * validate(chemin.iban, ({ value }) => erreurIban(value()));
2341
+ * ```
2342
+ *
2343
+ * Une fonction pure plutot qu'une regle toute faite : fournir la regle
2344
+ * importerait `validate` d'`@angular/forms` A L'EXECUTION, et le socle ne
2345
+ * l'importe que pour ses types (audit de la vague 3, V3-1).
2346
+ *
2347
+ * ⚠️ Confort de saisie, jamais controle (SEC-5) : le serveur revalide. Un champ
2348
+ * vide n'est pas « invalide » ici — c'est `required` qui le dit, avec le bon
2349
+ * message.
2350
+ */
2351
+ function erreurIban(valeur) {
2352
+ if (valeur.trim() === '')
2353
+ return null;
2354
+ return ibanEstValide(valeur) ? null : { kind: 'iban' };
2355
+ }
2356
+ /**
2357
+ * Erreur de FORME d'un numero de telephone : chiffres, `+` de tete facultatif,
2358
+ * de 4 a 15 chiffres — la longueur maximale d'un numero international (E.164).
2359
+ *
2360
+ * ```ts
2361
+ * validate(chemin.telephone, ({ value }) => erreurTelephone(value()));
2362
+ * ```
2363
+ *
2364
+ * ⚠️ Une forme valide ne dit pas que le numero existe : seul le serveur le sait.
2365
+ * Un champ vide n'est pas une erreur de forme : l'obligation est une autre regle.
2366
+ */
2367
+ function erreurTelephone(valeur) {
2368
+ const numero = normaliserTelephone(valeur);
2369
+ if (numero === '')
2370
+ return null;
2371
+ return /^\+?[0-9]{4,15}$/.test(numero) ? null : { kind: 'telephone' };
2372
+ }
2373
+ /**
2374
+ * Erreur de bornes d'une date, a brancher sur un schema :
2375
+ *
2376
+ * ```ts
2377
+ * validate(chemin.naissance, ({ value }) => erreurDate(value(), { max: aujourdHui }));
2378
+ * ```
2379
+ *
2380
+ * Les bornes du schema et celles passees au calendrier doivent etre les memes :
2381
+ * le calendrier grise, le schema refuse une date tapee a la main.
2382
+ */
2383
+ function erreurDate(valeur, bornes) {
2384
+ if (valeur === null || valeur === '')
2385
+ return null;
2386
+ const { min, max } = bornes;
2387
+ if (min !== undefined && min !== null && valeur < min) {
2388
+ return { kind: 'dateMin', min };
2389
+ }
2390
+ if (max !== undefined && max !== null && valeur > max) {
2391
+ return { kind: 'dateMax', max };
2392
+ }
2393
+ return null;
2394
+ }
2395
+ /** Une fin anterieure au debut. Une borne manquante n'est pas une erreur de plage. */
2396
+ function erreurPlageDeDates(plage) {
2397
+ const { debut, fin } = plage;
2398
+ if (debut === null || fin === null)
2399
+ return null;
2400
+ return fin < debut ? { kind: 'plageDeDates' } : null;
2401
+ }
2402
+ /**
2403
+ * Erreurs d'une liste de fichiers, une par fichier refuse, pour dire LEQUEL :
2404
+ *
2405
+ * ```ts
2406
+ * validate(chemin.pieces, ({ value }) => erreursDeFichiers(value(), regles));
2407
+ * ```
2408
+ *
2409
+ * ⚠️ Confort, jamais securite (SEC-5) : le type se deduit de l'extension, et le
2410
+ * serveur relit le contenu.
2411
+ */
2412
+ function erreursDeFichiers(fichiers, regles) {
2413
+ const erreurs = [];
2414
+ const { accept = '', tailleMax, nombreMax } = regles;
2415
+ if (nombreMax !== undefined && fichiers.length > nombreMax) {
2416
+ erreurs.push({ kind: 'fichiersNombre', max: nombreMax });
2417
+ }
2418
+ for (const fichier of fichiers) {
2419
+ if (!typeDeFichierAccepte(fichier, accept)) {
2420
+ erreurs.push({ kind: 'fichierType', fichier: fichier.name });
2421
+ }
2422
+ else if (tailleMax !== undefined && fichier.size > tailleMax) {
2423
+ erreurs.push({
2424
+ kind: 'fichierTaille',
2425
+ fichier: fichier.name,
2426
+ max: tailleMax,
2427
+ });
2428
+ }
2429
+ }
2430
+ return erreurs;
2431
+ }
2432
+ /**
2433
+ * Erreurs a afficher par un champ qui LIT sa saisie (`transformedValue`) : celles
2434
+ * du formulaire, plus les erreurs de lecture qu'il ne connait pas encore.
2435
+ *
2436
+ * ⚠️ Relie a un formulaire, un champ recoit ses erreurs de lecture DANS `errors`,
2437
+ * par Signal Forms. Employe seul, par `[(value)]`, il ne les recoit nulle part :
2438
+ * sans cette fusion, « 12,5,3 » s'afficherait sans le moindre message. Une erreur
2439
+ * deja presente — meme genre, meme partie — n'est pas repetee.
2440
+ */
2441
+ function fusionnerErreurs(formulaire, lecture) {
2442
+ const cle = (erreur) => `${erreur.kind}|${erreur.partie ?? ''}`;
2443
+ const connues = new Set(formulaire.map(cle));
2444
+ return [...formulaire, ...lecture.filter((erreur) => !connues.has(cle(erreur)))];
2445
+ }
2446
+
2447
+ /**
2448
+ * Base des champs du socle : le contrat Signal Forms, ecrit une fois — tout sauf
2449
+ * `value`, dont le type depend du champ (texte, montant, date).
2450
+ *
2451
+ * ⚠️ **Une base, et non une copie par champ.** Neuf membres de contrat recopies
2452
+ * dans six composants, c'est six occasions d'en oublier un — et l'oubli ne se
2453
+ * voit pas : un champ sans `touched` n'affiche jamais ses erreurs, un champ sans
2454
+ * reflet de `hidden` ne se masque pas, et rien n'echoue.
2455
+ *
2456
+ * Les noms restent anglais : voir le glossaire, § 8.
2457
+ *
2458
+ * Le gabarit du champ doit nommer son element natif `#champ`, et le brancher sur
2459
+ * `touch` a la perte du focus.
2460
+ */
2461
+ class ContratDeChamp {
2462
+ libelle = input.required(/* @ts-ignore */
2463
+ ...(ngDevMode ? [{ debugName: "libelle" }] : /* istanbul ignore next */ []));
2464
+ aide = input(null, /* @ts-ignore */
2465
+ ...(ngDevMode ? [{ debugName: "aide" }] : /* istanbul ignore next */ []));
2466
+ errors = input([], /* @ts-ignore */
2467
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
2468
+ touched = input(false, { ...(ngDevMode ? { debugName: "touched" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2469
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2470
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2471
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2472
+ hidden = input(false, { ...(ngDevMode ? { debugName: "hidden" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2473
+ name = input('', /* @ts-ignore */
2474
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
2475
+ touch = output();
2476
+ champ = viewChild('champ', /* @ts-ignore */
2477
+ ...(ngDevMode ? [{ debugName: "champ" }] : /* istanbul ignore next */ []));
2478
+ /** Appele par Signal Forms pour amener le focus sur un champ en erreur. */
2479
+ focus(options) {
2480
+ this.champ()?.nativeElement.focus(options);
2481
+ }
2482
+ texteSaisi(evenement) {
2483
+ return evenement.target.value;
2484
+ }
2485
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContratDeChamp, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2486
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.6", type: ContratDeChamp, isStandalone: true, inputs: { libelle: { classPropertyName: "libelle", publicName: "libelle", isSignal: true, isRequired: true, transformFunction: null }, aide: { classPropertyName: "aide", publicName: "aide", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touch: "touch" }, host: { properties: { "attr.hidden": "hidden() ? \"\" : null" } }, viewQueries: [{ propertyName: "champ", first: true, predicate: ["champ"], descendants: true, isSignal: true }], ngImport: i0 });
2487
+ }
2488
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ContratDeChamp, decorators: [{
2489
+ type: Directive,
2490
+ args: [{
2491
+ host: {
2492
+ // L'entree `hidden` du contrat n'ecrit pas l'attribut : on le reflete, sans
2493
+ // quoi la regle `:host([hidden])` du socle ne s'applique pas.
2494
+ '[attr.hidden]': 'hidden() ? "" : null',
2495
+ },
2496
+ }]
2497
+ }], propDecorators: { libelle: [{ type: i0.Input, args: [{ isSignal: true, alias: "libelle", required: true }] }], aide: [{ type: i0.Input, args: [{ isSignal: true, alias: "aide", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }], champ: [{ type: i0.ViewChild, args: ['champ', { isSignal: true }] }] } });
2498
+ /** Base des champs dont la valeur est le texte saisi. */
2499
+ class ControleDeSaisie extends ContratDeChamp {
2500
+ value = model('', /* @ts-ignore */
2501
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
2502
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ControleDeSaisie, deps: null, target: i0.ɵɵFactoryTarget.Directive });
2503
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: ControleDeSaisie, isStandalone: true, inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, usesInheritance: true, ngImport: i0 });
2504
+ }
2505
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ControleDeSaisie, decorators: [{
2506
+ type: Directive
2507
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
2508
+
2509
+ let compteur$6 = 0;
2510
+ /** Elements qu'un `label for` etiquette. */
2511
+ const ELEMENTS_ETIQUETABLES = new Set([
2512
+ 'INPUT',
2513
+ 'SELECT',
2514
+ 'TEXTAREA',
2515
+ 'BUTTON',
2516
+ 'METER',
2517
+ 'OUTPUT',
2518
+ 'PROGRESS',
2519
+ ]);
2520
+ /**
2521
+ * Etiquette, aide, erreur et controle d'un champ de saisie — et le cablage qui
2522
+ * les relie.
2523
+ *
2524
+ * ⚠️ **Le SEUL endroit ou ce cablage existe** (audit de la vague 3, V3-2). Chaque
2525
+ * champ du socle l'emploie au lieu de refaire identifiants, `aria-describedby` et
2526
+ * `aria-invalid` : c'est precisement le code que l'on croit fini et qui se perd,
2527
+ * un champ specialise a la fois.
2528
+ *
2529
+ * L'ordre visuel suit celui de la lecture : etiquette, aide, erreur, controle.
2530
+ * L'erreur est AU-DESSUS du controle, pour etre vue avant de retaper.
2531
+ *
2532
+ * ⚠️ **L'erreur n'est pas une region vivante.** Annoncee a la perte du focus, elle
2533
+ * couperait la lecture du champ suivant, ou la personne est deja passee. Elle est
2534
+ * reliee au controle, donc lue quand on y revient ; a l'envoi, c'est le
2535
+ * recapitulatif d'erreurs qui prend le focus et les enumere.
2536
+ */
2537
+ class StFormField {
2538
+ libelles = inject(ST_LIBELLES);
2539
+ locale = inject(ST_LOCALE);
2540
+ element = inject(ElementRef);
2541
+ /**
2542
+ * ⚠️ OBLIGATOIRE et toujours visible (UX-1). Un texte indicatif ne le remplace
2543
+ * jamais : il disparait a la premiere frappe.
2544
+ */
2545
+ libelle = input.required(/* @ts-ignore */
2546
+ ...(ngDevMode ? [{ debugName: "libelle" }] : /* istanbul ignore next */ []));
2547
+ /** Aide permanente : format attendu, exemple. Lue apres l'etiquette. */
2548
+ aide = input(null, /* @ts-ignore */
2549
+ ...(ngDevMode ? [{ debugName: "aide" }] : /* istanbul ignore next */ []));
2550
+ /** Texte complementaire tenu a jour par le controle, comme un compteur. */
2551
+ complement = input(null, /* @ts-ignore */
2552
+ ...(ngDevMode ? [{ debugName: "complement" }] : /* istanbul ignore next */ []));
2553
+ erreurs = input([], /* @ts-ignore */
2554
+ ...(ngDevMode ? [{ debugName: "erreurs" }] : /* istanbul ignore next */ []));
2555
+ /**
2556
+ * ⚠️ Faux tant que la personne n'a pas quitte le champ ou tente l'envoi (UX-3) :
2557
+ * un formulaire vierge couvert d'erreurs accuse avant qu'on ait rien fait.
2558
+ */
2559
+ afficherErreurs = input(false, { ...(ngDevMode ? { debugName: "afficherErreurs" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2560
+ obligatoire = input(false, { ...(ngDevMode ? { debugName: "obligatoire" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2561
+ /**
2562
+ * ⚠️ Change l'ordre VISUEL, jamais celui du document : l'etiquette reste
2563
+ * reliee par `for`, et l'ordre de tabulation ne passe que par le controle.
2564
+ */
2565
+ disposition = input('empilee', /* @ts-ignore */
2566
+ ...(ngDevMode ? [{ debugName: "disposition" }] : /* istanbul ignore next */ []));
2567
+ /**
2568
+ * Etiquette lue mais non affichee.
2569
+ *
2570
+ * ⚠️ Reservee a la RECHERCHE, ou l'icone, le bouton et la position en tete de
2571
+ * page disent deja le but du champ. Partout ailleurs, une etiquette masquee est
2572
+ * l'etiquette qui manque au moment ou l'on tape (UX-1).
2573
+ */
2574
+ libelleMasque = input(false, { ...(ngDevMode ? { debugName: "libelleMasque" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2575
+ /** Identifiants du champ, lus par `stControleDeChamp` et par le gabarit. */
2576
+ identifiants = (() => {
2577
+ const base = `st-champ-${(compteur$6 += 1).toString()}`;
2578
+ return {
2579
+ controle: base,
2580
+ libelle: `${base}-libelle`,
2581
+ aide: `${base}-aide`,
2582
+ complement: `${base}-complement`,
2583
+ erreur: `${base}-erreur`,
2584
+ };
2585
+ })();
2586
+ libelleObligatoire = computed(() => this.libelles().obligatoire, /* @ts-ignore */
2587
+ ...(ngDevMode ? [{ debugName: "libelleObligatoire" }] : /* istanbul ignore next */ []));
2588
+ /** Messages a afficher, dans la langue courante. Vide tant qu'ils ne doivent pas l'etre. */
2589
+ messages = computed(() => {
2590
+ if (!this.afficherErreurs())
2591
+ return [];
2592
+ const libelles = this.libelles();
2593
+ const locale = this.locale();
2594
+ return this.erreurs().map((erreur) => messageDErreur(erreur, libelles, locale));
2595
+ }, /* @ts-ignore */
2596
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
2597
+ /**
2598
+ * Un clic sur l'etiquette d'un controle non natif n'y mene pas de lui-meme :
2599
+ * on y amene le focus, comme le navigateur le fait pour un champ natif.
2600
+ */
2601
+ focaliserLeControle() {
2602
+ const controle = this.element.nativeElement.querySelector(`#${this.identifiants.controle}`);
2603
+ if (controle !== null && !ELEMENTS_ETIQUETABLES.has(controle.tagName))
2604
+ controle.focus();
2605
+ }
2606
+ /** Lu par le controle pour `aria-invalid`. */
2607
+ enErreur = computed(() => this.messages().length > 0, /* @ts-ignore */
2608
+ ...(ngDevMode ? [{ debugName: "enErreur" }] : /* istanbul ignore next */ []));
2609
+ /**
2610
+ * ⚠️ Seuls les elements PRESENTS sont references : un `aria-describedby` qui
2611
+ * vise un identifiant absent est ignore par certains lecteurs d'ecran — tout
2612
+ * entier, y compris les references valides qui l'accompagnent.
2613
+ */
2614
+ descriptions = computed(() => {
2615
+ const ids = [];
2616
+ if (this.aide() !== null)
2617
+ ids.push(this.identifiants.aide);
2618
+ if (this.complement() !== null)
2619
+ ids.push(this.identifiants.complement);
2620
+ if (this.enErreur())
2621
+ ids.push(this.identifiants.erreur);
2622
+ return ids.length === 0 ? null : ids.join(' ');
2623
+ }, /* @ts-ignore */
2624
+ ...(ngDevMode ? [{ debugName: "descriptions" }] : /* istanbul ignore next */ []));
2625
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StFormField, deps: [], target: i0.ɵɵFactoryTarget.Component });
2626
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StFormField, isStandalone: true, selector: "st-form-field", inputs: { libelle: { classPropertyName: "libelle", publicName: "libelle", isSignal: true, isRequired: true, transformFunction: null }, aide: { classPropertyName: "aide", publicName: "aide", isSignal: true, isRequired: false, transformFunction: null }, complement: { classPropertyName: "complement", publicName: "complement", isSignal: true, isRequired: false, transformFunction: null }, erreurs: { classPropertyName: "erreurs", publicName: "erreurs", isSignal: true, isRequired: false, transformFunction: null }, afficherErreurs: { classPropertyName: "afficherErreurs", publicName: "afficherErreurs", isSignal: true, isRequired: false, transformFunction: null }, obligatoire: { classPropertyName: "obligatoire", publicName: "obligatoire", isSignal: true, isRequired: false, transformFunction: null }, disposition: { classPropertyName: "disposition", publicName: "disposition", isSignal: true, isRequired: false, transformFunction: null }, libelleMasque: { classPropertyName: "libelleMasque", publicName: "libelleMasque", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-en-erreur": "enErreur() ? \"\" : null", "attr.data-disposition": "disposition()" }, classAttribute: "st-form-field" }, ngImport: i0, template: "<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus --\n Le clic sur une etiquette est une commodite de souris, comme pour tout\n champ natif : au clavier, le controle se rejoint par tabulation, et\n l'etiquette n'a pas a recevoir le focus. -->\n<label\n class=\"st-form-field__libelle\"\n [class.st-vh]=\"libelleMasque()\"\n [id]=\"identifiants.libelle\"\n [for]=\"identifiants.controle\"\n [attr.data-libelle]=\"libelle()\"\n (click)=\"focaliserLeControle()\"\n>\n {{ libelle() }}\n @if (obligatoire()) {\n <span class=\"st-form-field__obligatoire\">{{ libelleObligatoire() }}</span>\n }\n</label>\n\n@if (aide(); as texte) {\n <p class=\"st-form-field__aide\" [id]=\"identifiants.aide\">{{ texte }}</p>\n}\n\n@if (enErreur()) {\n <div class=\"st-form-field__erreurs\" [id]=\"identifiants.erreur\">\n @for (message of messages(); track $index) {\n <st-inline-message ton=\"danger\">{{ message }}</st-inline-message>\n }\n </div>\n}\n\n<div class=\"st-form-field__controle\">\n <ng-content />\n</div>\n\n@if (complement(); as texte) {\n <p class=\"st-form-field__complement\" [id]=\"identifiants.complement\">{{ texte }}</p>\n}\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:grid;grid-template-areas:\"libelle\" \"aide\" \"erreur\" \"controle\" \"complement\";grid-template-columns:minmax(0,1fr);gap:var(--st-espacement-1);margin-block-end:var(--st-espacement-5)}:host([data-disposition=case]){grid-template-areas:\"erreur erreur\" \"controle libelle\" \". aide\" \". complement\";grid-template-columns:auto minmax(0,1fr);column-gap:var(--st-espacement-2);align-items:center}.st-form-field__libelle{grid-area:libelle}.st-form-field__aide{grid-area:aide}.st-form-field__erreurs{grid-area:erreur}.st-form-field__controle{grid-area:controle}.st-form-field__complement{grid-area:complement}:host([data-disposition=case]) .st-form-field__libelle{font-weight:var(--st-graisse-normale)}.st-form-field__libelle{font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte)}.st-form-field__obligatoire{margin-inline-start:var(--st-espacement-1);font-weight:var(--st-graisse-normale);color:var(--st-couleur-texte-discret)}.st-form-field__aide,.st-form-field__complement{margin:0;font-size:var(--st-taille-sm);color:var(--st-couleur-texte-discret)}:host([data-en-erreur]){padding-inline-start:var(--st-espacement-3);border-inline-start:3px solid var(--st-couleur-danger)}\n"], dependencies: [{ kind: "component", type: StInlineMessage, selector: "st-inline-message", inputs: ["ton", "identifiant"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2627
+ }
2628
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StFormField, decorators: [{
2629
+ type: Component,
2630
+ args: [{ selector: 'st-form-field', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StInlineMessage], host: {
2631
+ class: 'st-form-field',
2632
+ '[attr.data-en-erreur]': 'enErreur() ? "" : null',
2633
+ '[attr.data-disposition]': 'disposition()',
2634
+ }, template: "<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus --\n Le clic sur une etiquette est une commodite de souris, comme pour tout\n champ natif : au clavier, le controle se rejoint par tabulation, et\n l'etiquette n'a pas a recevoir le focus. -->\n<label\n class=\"st-form-field__libelle\"\n [class.st-vh]=\"libelleMasque()\"\n [id]=\"identifiants.libelle\"\n [for]=\"identifiants.controle\"\n [attr.data-libelle]=\"libelle()\"\n (click)=\"focaliserLeControle()\"\n>\n {{ libelle() }}\n @if (obligatoire()) {\n <span class=\"st-form-field__obligatoire\">{{ libelleObligatoire() }}</span>\n }\n</label>\n\n@if (aide(); as texte) {\n <p class=\"st-form-field__aide\" [id]=\"identifiants.aide\">{{ texte }}</p>\n}\n\n@if (enErreur()) {\n <div class=\"st-form-field__erreurs\" [id]=\"identifiants.erreur\">\n @for (message of messages(); track $index) {\n <st-inline-message ton=\"danger\">{{ message }}</st-inline-message>\n }\n </div>\n}\n\n<div class=\"st-form-field__controle\">\n <ng-content />\n</div>\n\n@if (complement(); as texte) {\n <p class=\"st-form-field__complement\" [id]=\"identifiants.complement\">{{ texte }}</p>\n}\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:grid;grid-template-areas:\"libelle\" \"aide\" \"erreur\" \"controle\" \"complement\";grid-template-columns:minmax(0,1fr);gap:var(--st-espacement-1);margin-block-end:var(--st-espacement-5)}:host([data-disposition=case]){grid-template-areas:\"erreur erreur\" \"controle libelle\" \". aide\" \". complement\";grid-template-columns:auto minmax(0,1fr);column-gap:var(--st-espacement-2);align-items:center}.st-form-field__libelle{grid-area:libelle}.st-form-field__aide{grid-area:aide}.st-form-field__erreurs{grid-area:erreur}.st-form-field__controle{grid-area:controle}.st-form-field__complement{grid-area:complement}:host([data-disposition=case]) .st-form-field__libelle{font-weight:var(--st-graisse-normale)}.st-form-field__libelle{font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte)}.st-form-field__obligatoire{margin-inline-start:var(--st-espacement-1);font-weight:var(--st-graisse-normale);color:var(--st-couleur-texte-discret)}.st-form-field__aide,.st-form-field__complement{margin:0;font-size:var(--st-taille-sm);color:var(--st-couleur-texte-discret)}:host([data-en-erreur]){padding-inline-start:var(--st-espacement-3);border-inline-start:3px solid var(--st-couleur-danger)}\n"] }]
2635
+ }], propDecorators: { libelle: [{ type: i0.Input, args: [{ isSignal: true, alias: "libelle", required: true }] }], aide: [{ type: i0.Input, args: [{ isSignal: true, alias: "aide", required: false }] }], complement: [{ type: i0.Input, args: [{ isSignal: true, alias: "complement", required: false }] }], erreurs: [{ type: i0.Input, args: [{ isSignal: true, alias: "erreurs", required: false }] }], afficherErreurs: [{ type: i0.Input, args: [{ isSignal: true, alias: "afficherErreurs", required: false }] }], obligatoire: [{ type: i0.Input, args: [{ isSignal: true, alias: "obligatoire", required: false }] }], disposition: [{ type: i0.Input, args: [{ isSignal: true, alias: "disposition", required: false }] }], libelleMasque: [{ type: i0.Input, args: [{ isSignal: true, alias: "libelleMasque", required: false }] }] } });
2636
+ /**
2637
+ * Pose sur l'element natif d'un champ le cablage de son `st-form-field` :
2638
+ * identifiant, description, invalidite, obligation.
2639
+ *
2640
+ * ⚠️ `aria-required` et NON l'attribut `required` natif. Le natif declenche la
2641
+ * validation du navigateur a l'envoi : une bulle « Veuillez renseigner ce champ »
2642
+ * dans la langue du NAVIGATEUR, pas dans celle de l'application, et qui double
2643
+ * le message du socle.
2644
+ */
2645
+ class StControleDeChamp {
2646
+ champ = inject(StFormField);
2647
+ /**
2648
+ * ⚠️ `label for` n'etiquette que les elements natifs de formulaire. Le
2649
+ * declencheur d'une selection multiple est un `div` de role `combobox` : sans
2650
+ * `aria-labelledby`, il n'aurait aucun nom, et l'etiquette visible ne dirait
2651
+ * rien au lecteur d'ecran.
2652
+ */
2653
+ etiquetable = ELEMENTS_ETIQUETABLES.has(inject(ElementRef).nativeElement.tagName);
2654
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StControleDeChamp, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2655
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: StControleDeChamp, isStandalone: true, selector: "[stControleDeChamp]", host: { properties: { "id": "champ.identifiants.controle", "attr.aria-labelledby": "etiquetable ? null : champ.identifiants.libelle", "attr.aria-describedby": "champ.descriptions()", "attr.aria-invalid": "champ.enErreur() ? \"true\" : null", "attr.aria-required": "champ.obligatoire() ? \"true\" : null" } }, ngImport: i0 });
2656
+ }
2657
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StControleDeChamp, decorators: [{
2658
+ type: Directive,
2659
+ args: [{
2660
+ selector: '[stControleDeChamp]',
2661
+ host: {
2662
+ '[id]': 'champ.identifiants.controle',
2663
+ '[attr.aria-labelledby]': 'etiquetable ? null : champ.identifiants.libelle',
2664
+ '[attr.aria-describedby]': 'champ.descriptions()',
2665
+ '[attr.aria-invalid]': 'champ.enErreur() ? "true" : null',
2666
+ '[attr.aria-required]': 'champ.obligatoire() ? "true" : null',
2667
+ },
2668
+ }]
2669
+ }] });
2670
+
2671
+ /**
2672
+ * Champ de saisie d'une ligne, integre a Signal Forms.
2673
+ *
2674
+ * ```html
2675
+ * <st-input libelle="Nom du beneficiaire" autocomplete="name" [formField]="virement.beneficiaire" />
2676
+ * <!-- ou, sans formulaire : -->
2677
+ * <st-input libelle="Reference" [(value)]="reference" />
2678
+ * ```
2679
+ *
2680
+ * ⚠️ **Contrat Signal Forms, noms anglais.** `value`, `errors`, `touched`,
2681
+ * `disabled`… sont cherches PAR LEUR NOM par la directive `[formField]` : les
2682
+ * traduire romprait l'integration sans la moindre erreur (audit de la vague 3,
2683
+ * V3-1). L'import d'`@angular/forms` est un import de TYPE : aucun code n'entre
2684
+ * dans le paquet.
2685
+ *
2686
+ * ⚠️ **Pas d'attribut `maxlength` natif.** Il TRONQUE en silence un texte colle
2687
+ * trop long : la personne croit avoir saisi sa reference entiere, et n'en envoie
2688
+ * que le debut. La limite passe par la validation, qui DIT le depassement.
2689
+ */
2690
+ class StInput extends ControleDeSaisie {
2691
+ type = input('text', /* @ts-ignore */
2692
+ ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
2693
+ /**
2694
+ * WCAG 1.3.5 : identifie le but du champ, pour le remplissage automatique et les
2695
+ * aides techniques. `null` : laisse le navigateur decider.
2696
+ */
2697
+ autocomplete = input(null, /* @ts-ignore */
2698
+ ...(ngDevMode ? [{ debugName: "autocomplete" }] : /* istanbul ignore next */ []));
2699
+ /** Clavier virtuel adapte : `numeric`, `decimal`, `email`… */
2700
+ inputmode = input(null, /* @ts-ignore */
2701
+ ...(ngDevMode ? [{ debugName: "inputmode" }] : /* istanbul ignore next */ []));
2702
+ surSaisie(evenement) {
2703
+ this.value.set(this.texteSaisi(evenement));
2704
+ }
2705
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StInput, deps: null, target: i0.ɵɵFactoryTarget.Component });
2706
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StInput, isStandalone: true, selector: "st-input", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, inputmode: { classPropertyName: "inputmode", publicName: "inputmode", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "st-input" }, usesInheritance: true, ngImport: i0, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <input\n #champ\n class=\"st-input__champ\"\n stControleDeChamp\n [type]=\"type()\"\n [value]=\"value()\"\n [attr.name]=\"name() || null\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.inputmode]=\"inputmode()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"touch.emit()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-input__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-input__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-input__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-input__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-input__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2707
+ }
2708
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StInput, decorators: [{
2709
+ type: Component,
2710
+ args: [{ selector: 'st-input', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp], host: { class: 'st-input' }, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <input\n #champ\n class=\"st-input__champ\"\n stControleDeChamp\n [type]=\"type()\"\n [value]=\"value()\"\n [attr.name]=\"name() || null\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.inputmode]=\"inputmode()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"touch.emit()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-input__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-input__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-input__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-input__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-input__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}\n"] }]
2711
+ }], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], inputmode: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputmode", required: false }] }] } });
2712
+
2713
+ /** Restant a partir duquel le compteur se signale. */
2714
+ const SEUILS_D_ANNONCE = [20, 10, 0];
2715
+ /**
2716
+ * Zone de texte, integree a Signal Forms, avec compteur quand une longueur
2717
+ * maximale est fixee.
2718
+ *
2719
+ * ⚠️ **Le compteur n'est pas annonce a chaque frappe** : une region vivante qui
2720
+ * change a chaque caractere recouvre ce que la personne tape. Il est RELIE a la
2721
+ * zone — donc lu quand on y revient — et seuls les seuils (20, 10, 0 restants)
2722
+ * et le depassement s'annoncent (UX-11).
2723
+ *
2724
+ * ⚠️ **Pas de `maxlength` natif**, meme raison que `st-input` : un texte colle
2725
+ * serait tronque en silence. Le compteur affiche le depassement, la validation
2726
+ * le refuse.
2727
+ */
2728
+ class StTextarea extends ControleDeSaisie {
2729
+ libelles = inject(ST_LIBELLES);
2730
+ lignes = input(4, /* @ts-ignore */
2731
+ ...(ngDevMode ? [{ debugName: "lignes" }] : /* istanbul ignore next */ []));
2732
+ /** Contrat Signal Forms : la longueur maximale fixee par le schema. */
2733
+ maxLength = input(undefined, /* @ts-ignore */
2734
+ ...(ngDevMode ? [{ debugName: "maxLength" }] : /* istanbul ignore next */ []));
2735
+ annonce = new AnnonceLocale(inject(DestroyRef));
2736
+ restants = computed(() => {
2737
+ const maximum = this.maxLength();
2738
+ return maximum === undefined ? null : maximum - this.value().length;
2739
+ }, /* @ts-ignore */
2740
+ ...(ngDevMode ? [{ debugName: "restants" }] : /* istanbul ignore next */ []));
2741
+ compteur = computed(() => {
2742
+ const restants = this.restants();
2743
+ if (restants === null)
2744
+ return null;
2745
+ const libelles = this.libelles();
2746
+ return restants >= 0
2747
+ ? libelles.caracteresRestants(restants)
2748
+ : libelles.caracteresEnTrop(-restants);
2749
+ }, /* @ts-ignore */
2750
+ ...(ngDevMode ? [{ debugName: "compteur" }] : /* istanbul ignore next */ []));
2751
+ constructor() {
2752
+ super();
2753
+ let precedent = null;
2754
+ effect(() => {
2755
+ const restants = this.restants();
2756
+ if (restants === null)
2757
+ return;
2758
+ const avant = precedent;
2759
+ precedent = restants;
2760
+ if (avant === null)
2761
+ return;
2762
+ const franchi = SEUILS_D_ANNONCE.some((seuil) => avant > seuil && restants <= seuil) ||
2763
+ (avant >= 0 && restants < 0);
2764
+ const texte = this.compteur();
2765
+ if (franchi && texte !== null)
2766
+ this.annonce.annoncer(texte);
2767
+ });
2768
+ }
2769
+ surSaisie(evenement) {
2770
+ this.value.set(this.texteSaisi(evenement));
2771
+ }
2772
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StTextarea, deps: [], target: i0.ɵɵFactoryTarget.Component });
2773
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StTextarea, isStandalone: true, selector: "st-textarea", inputs: { lignes: { classPropertyName: "lignes", publicName: "lignes", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "st-textarea" }, usesInheritance: true, ngImport: i0, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [complement]=\"compteur()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <textarea\n #champ\n class=\"st-textarea__champ\"\n stControleDeChamp\n [rows]=\"lignes()\"\n [value]=\"value()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"touch.emit()\"\n ></textarea>\n</st-form-field>\n<span class=\"st-vh\" aria-live=\"polite\">{{ annonce.texte() }}</span>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-textarea__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-textarea__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-textarea__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-textarea__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-textarea__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-textarea__champ{resize:vertical}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2774
+ }
2775
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StTextarea, decorators: [{
2776
+ type: Component,
2777
+ args: [{ selector: 'st-textarea', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp], host: { class: 'st-textarea' }, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [complement]=\"compteur()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <textarea\n #champ\n class=\"st-textarea__champ\"\n stControleDeChamp\n [rows]=\"lignes()\"\n [value]=\"value()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"touch.emit()\"\n ></textarea>\n</st-form-field>\n<span class=\"st-vh\" aria-live=\"polite\">{{ annonce.texte() }}</span>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-textarea__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-textarea__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-textarea__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-textarea__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-textarea__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-textarea__champ{resize:vertical}\n"] }]
2778
+ }], ctorParameters: () => [], propDecorators: { lignes: [{ type: i0.Input, args: [{ isSignal: true, alias: "lignes", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }] } });
2779
+
2780
+ let compteur$5 = 0;
2781
+ /**
2782
+ * Groupe de controles, nomme par sa QUESTION.
2783
+ *
2784
+ * ⚠️ `fieldset` et `legend` natifs (UX-7). Sans legende, un lecteur d'ecran
2785
+ * annonce « Oui, bouton radio » et « Non, bouton radio » : la reponse, jamais la
2786
+ * question. La legende est lue a l'entree dans le groupe.
2787
+ *
2788
+ * C'est le pendant de `st-form-field` pour un GROUPE : l'aide et l'erreur sont
2789
+ * reliees au `fieldset` par `aria-describedby`, et l'erreur porte sur le groupe
2790
+ * entier — « Choisissez un mode de reglement » — et non sur chaque option.
2791
+ *
2792
+ * ⚠️ L'obligation se dit dans la legende, en texte : `aria-required` n'est pas
2793
+ * valide sur un groupe, et `required` natif ferait parler le navigateur dans sa
2794
+ * propre langue.
2795
+ */
2796
+ class StFieldset {
2797
+ libelles = inject(ST_LIBELLES);
2798
+ locale = inject(ST_LOCALE);
2799
+ /** La question posee par le groupe. Obligatoire. */
2800
+ legende = input.required(/* @ts-ignore */
2801
+ ...(ngDevMode ? [{ debugName: "legende" }] : /* istanbul ignore next */ []));
2802
+ aide = input(null, /* @ts-ignore */
2803
+ ...(ngDevMode ? [{ debugName: "aide" }] : /* istanbul ignore next */ []));
2804
+ erreurs = input([], /* @ts-ignore */
2805
+ ...(ngDevMode ? [{ debugName: "erreurs" }] : /* istanbul ignore next */ []));
2806
+ afficherErreurs = input(false, { ...(ngDevMode ? { debugName: "afficherErreurs" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2807
+ obligatoire = input(false, { ...(ngDevMode ? { debugName: "obligatoire" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2808
+ base = `st-groupe-${(compteur$5 += 1).toString()}`;
2809
+ identifiantAide = `${this.base}-aide`;
2810
+ identifiantErreur = `${this.base}-erreur`;
2811
+ libelleObligatoire = computed(() => this.libelles().obligatoire, /* @ts-ignore */
2812
+ ...(ngDevMode ? [{ debugName: "libelleObligatoire" }] : /* istanbul ignore next */ []));
2813
+ messages = computed(() => {
2814
+ if (!this.afficherErreurs())
2815
+ return [];
2816
+ const libelles = this.libelles();
2817
+ const locale = this.locale();
2818
+ return this.erreurs().map((erreur) => messageDErreur(erreur, libelles, locale));
2819
+ }, /* @ts-ignore */
2820
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
2821
+ enErreur = computed(() => this.messages().length > 0, /* @ts-ignore */
2822
+ ...(ngDevMode ? [{ debugName: "enErreur" }] : /* istanbul ignore next */ []));
2823
+ descriptions = computed(() => {
2824
+ const ids = [];
2825
+ if (this.aide() !== null)
2826
+ ids.push(this.identifiantAide);
2827
+ if (this.enErreur())
2828
+ ids.push(this.identifiantErreur);
2829
+ return ids.length === 0 ? null : ids.join(' ');
2830
+ }, /* @ts-ignore */
2831
+ ...(ngDevMode ? [{ debugName: "descriptions" }] : /* istanbul ignore next */ []));
2832
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StFieldset, deps: [], target: i0.ɵɵFactoryTarget.Component });
2833
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StFieldset, isStandalone: true, selector: "st-fieldset", inputs: { legende: { classPropertyName: "legende", publicName: "legende", isSignal: true, isRequired: true, transformFunction: null }, aide: { classPropertyName: "aide", publicName: "aide", isSignal: true, isRequired: false, transformFunction: null }, erreurs: { classPropertyName: "erreurs", publicName: "erreurs", isSignal: true, isRequired: false, transformFunction: null }, afficherErreurs: { classPropertyName: "afficherErreurs", publicName: "afficherErreurs", isSignal: true, isRequired: false, transformFunction: null }, obligatoire: { classPropertyName: "obligatoire", publicName: "obligatoire", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-en-erreur": "enErreur() ? \"\" : null" }, classAttribute: "st-fieldset" }, ngImport: i0, template: "<fieldset class=\"st-fieldset__groupe\" [attr.aria-describedby]=\"descriptions()\">\n <legend class=\"st-fieldset__legende\" [attr.data-libelle]=\"legende()\">\n {{ legende() }}\n @if (obligatoire()) {\n <span class=\"st-fieldset__obligatoire\">{{ libelleObligatoire() }}</span>\n }\n </legend>\n\n @if (aide(); as texte) {\n <p class=\"st-fieldset__aide\" [id]=\"identifiantAide\">{{ texte }}</p>\n }\n\n @if (enErreur()) {\n <div class=\"st-fieldset__erreurs\" [id]=\"identifiantErreur\">\n @for (message of messages(); track $index) {\n <st-inline-message ton=\"danger\">{{ message }}</st-inline-message>\n }\n </div>\n }\n\n <div class=\"st-fieldset__controles\">\n <ng-content />\n </div>\n</fieldset>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block;margin-block-end:var(--st-espacement-5)}.st-fieldset__groupe{min-inline-size:0;padding:0;margin:0;border:0}.st-fieldset__legende{padding:0;margin-block-end:var(--st-espacement-2);font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte)}.st-fieldset__obligatoire{margin-inline-start:var(--st-espacement-1);font-weight:var(--st-graisse-normale);color:var(--st-couleur-texte-discret)}.st-fieldset__aide{margin:0 0 var(--st-espacement-2);font-size:var(--st-taille-sm);color:var(--st-couleur-texte-discret)}.st-fieldset__erreurs{margin-block-end:var(--st-espacement-2)}.st-fieldset__controles{display:flex;flex-direction:column;gap:var(--st-espacement-2)}:host([data-en-erreur]) .st-fieldset__groupe{padding-inline-start:var(--st-espacement-3);border-inline-start:3px solid var(--st-couleur-danger)}\n"], dependencies: [{ kind: "component", type: StInlineMessage, selector: "st-inline-message", inputs: ["ton", "identifiant"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2834
+ }
2835
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StFieldset, decorators: [{
2836
+ type: Component,
2837
+ args: [{ selector: 'st-fieldset', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StInlineMessage], host: {
2838
+ class: 'st-fieldset',
2839
+ '[attr.data-en-erreur]': 'enErreur() ? "" : null',
2840
+ }, template: "<fieldset class=\"st-fieldset__groupe\" [attr.aria-describedby]=\"descriptions()\">\n <legend class=\"st-fieldset__legende\" [attr.data-libelle]=\"legende()\">\n {{ legende() }}\n @if (obligatoire()) {\n <span class=\"st-fieldset__obligatoire\">{{ libelleObligatoire() }}</span>\n }\n </legend>\n\n @if (aide(); as texte) {\n <p class=\"st-fieldset__aide\" [id]=\"identifiantAide\">{{ texte }}</p>\n }\n\n @if (enErreur()) {\n <div class=\"st-fieldset__erreurs\" [id]=\"identifiantErreur\">\n @for (message of messages(); track $index) {\n <st-inline-message ton=\"danger\">{{ message }}</st-inline-message>\n }\n </div>\n }\n\n <div class=\"st-fieldset__controles\">\n <ng-content />\n </div>\n</fieldset>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block;margin-block-end:var(--st-espacement-5)}.st-fieldset__groupe{min-inline-size:0;padding:0;margin:0;border:0}.st-fieldset__legende{padding:0;margin-block-end:var(--st-espacement-2);font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte)}.st-fieldset__obligatoire{margin-inline-start:var(--st-espacement-1);font-weight:var(--st-graisse-normale);color:var(--st-couleur-texte-discret)}.st-fieldset__aide{margin:0 0 var(--st-espacement-2);font-size:var(--st-taille-sm);color:var(--st-couleur-texte-discret)}.st-fieldset__erreurs{margin-block-end:var(--st-espacement-2)}.st-fieldset__controles{display:flex;flex-direction:column;gap:var(--st-espacement-2)}:host([data-en-erreur]) .st-fieldset__groupe{padding-inline-start:var(--st-espacement-3);border-inline-start:3px solid var(--st-couleur-danger)}\n"] }]
2841
+ }], propDecorators: { legende: [{ type: i0.Input, args: [{ isSignal: true, alias: "legende", required: true }] }], aide: [{ type: i0.Input, args: [{ isSignal: true, alias: "aide", required: false }] }], erreurs: [{ type: i0.Input, args: [{ isSignal: true, alias: "erreurs", required: false }] }], afficherErreurs: [{ type: i0.Input, args: [{ isSignal: true, alias: "afficherErreurs", required: false }] }], obligatoire: [{ type: i0.Input, args: [{ isSignal: true, alias: "obligatoire", required: false }] }] } });
2842
+
2843
+ /**
2844
+ * Case a cocher, integree a Signal Forms.
2845
+ *
2846
+ * ```html
2847
+ * <st-checkbox libelle="J'accepte les conditions generales" [formField]="inscription.conditions" />
2848
+ * ```
2849
+ *
2850
+ * Un element `input type="checkbox"` natif : Espace coche, l'etat est annonce,
2851
+ * et la case suit le reglage de contraste du systeme sans rien ecrire.
2852
+ *
2853
+ * ⚠️ **Pour un choix a envoyer avec un formulaire.** Un reglage qui s'applique
2854
+ * IMMEDIATEMENT est un `st-switch` : la case promet un effet a l'envoi, et
2855
+ * l'interrupteur un effet tout de suite (UX-8).
2856
+ */
2857
+ class StCheckbox {
2858
+ libelle = input.required(/* @ts-ignore */
2859
+ ...(ngDevMode ? [{ debugName: "libelle" }] : /* istanbul ignore next */ []));
2860
+ aide = input(null, /* @ts-ignore */
2861
+ ...(ngDevMode ? [{ debugName: "aide" }] : /* istanbul ignore next */ []));
2862
+ /** Contrat Signal Forms. */
2863
+ checked = model(false, /* @ts-ignore */
2864
+ ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
2865
+ errors = input([], /* @ts-ignore */
2866
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
2867
+ touched = input(false, { ...(ngDevMode ? { debugName: "touched" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2868
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2869
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2870
+ hidden = input(false, { ...(ngDevMode ? { debugName: "hidden" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2871
+ name = input('', /* @ts-ignore */
2872
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
2873
+ touch = output();
2874
+ champ = viewChild.required('champ', /* @ts-ignore */
2875
+ ...(ngDevMode ? [{ debugName: "champ" }] : /* istanbul ignore next */ []));
2876
+ focus(options) {
2877
+ this.champ().nativeElement.focus(options);
2878
+ }
2879
+ surChangement(evenement) {
2880
+ this.checked.set(evenement.target.checked);
2881
+ }
2882
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StCheckbox, deps: [], target: i0.ɵɵFactoryTarget.Component });
2883
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.6", type: StCheckbox, isStandalone: true, selector: "st-checkbox", inputs: { libelle: { classPropertyName: "libelle", publicName: "libelle", isSignal: true, isRequired: true, transformFunction: null }, aide: { classPropertyName: "aide", publicName: "aide", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", touch: "touch" }, host: { properties: { "attr.hidden": "hidden() ? \"\" : null" }, classAttribute: "st-checkbox" }, viewQueries: [{ propertyName: "champ", first: true, predicate: ["champ"], descendants: true, isSignal: true }], ngImport: i0, template: "<st-form-field\n disposition=\"case\"\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <input\n #champ\n class=\"st-checkbox__case\"\n type=\"checkbox\"\n stControleDeChamp\n [checked]=\"checked()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n (change)=\"surChangement($event)\"\n (blur)=\"touch.emit()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-checkbox__case:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-checkbox__case{inline-size:1.5rem;block-size:1.5rem;margin:0;cursor:pointer;accent-color:var(--st-couleur-primaire)}.st-checkbox__case:disabled{cursor:not-allowed}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2884
+ }
2885
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StCheckbox, decorators: [{
2886
+ type: Component,
2887
+ args: [{ selector: 'st-checkbox', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp], host: {
2888
+ class: 'st-checkbox',
2889
+ '[attr.hidden]': 'hidden() ? "" : null',
2890
+ }, template: "<st-form-field\n disposition=\"case\"\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <input\n #champ\n class=\"st-checkbox__case\"\n type=\"checkbox\"\n stControleDeChamp\n [checked]=\"checked()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n (change)=\"surChangement($event)\"\n (blur)=\"touch.emit()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-checkbox__case:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-checkbox__case{inline-size:1.5rem;block-size:1.5rem;margin:0;cursor:pointer;accent-color:var(--st-couleur-primaire)}.st-checkbox__case:disabled{cursor:not-allowed}\n"] }]
2891
+ }], propDecorators: { libelle: [{ type: i0.Input, args: [{ isSignal: true, alias: "libelle", required: true }] }], aide: [{ type: i0.Input, args: [{ isSignal: true, alias: "aide", required: false }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }], champ: [{ type: i0.ViewChild, args: ['champ', { isSignal: true }] }] } });
2892
+
2893
+ let compteur$4 = 0;
2894
+ /**
2895
+ * Groupe de boutons radio, integre a Signal Forms.
2896
+ *
2897
+ * ```html
2898
+ * <st-radio-group legende="Mode de reglement" [options]="modes" [formField]="commande.mode" />
2899
+ * ```
2900
+ *
2901
+ * ⚠️ **De vrais boutons radio natifs** : un seul arret de tabulation pour le
2902
+ * groupe, deplacement aux fleches, « 2 sur 3 » annonce. Les recrire en ARIA,
2903
+ * c'est cent lignes pour obtenir moins bien — meme raison que `st-theme-toggle`.
2904
+ *
2905
+ * ⚠️ **L'erreur porte sur le groupe**, par `st-fieldset`, et non sur chaque option :
2906
+ * « Choisissez un mode de reglement », une fois.
2907
+ *
2908
+ * `touch` n'est emis que lorsque le focus QUITTE le groupe : passer d'une option a
2909
+ * l'autre aux fleches n'est pas une fin de saisie, et l'erreur n'a pas a surgir
2910
+ * pendant qu'on choisit.
2911
+ */
2912
+ class StRadioGroup {
2913
+ element = inject(ElementRef);
2914
+ legende = input.required(/* @ts-ignore */
2915
+ ...(ngDevMode ? [{ debugName: "legende" }] : /* istanbul ignore next */ []));
2916
+ aide = input(null, /* @ts-ignore */
2917
+ ...(ngDevMode ? [{ debugName: "aide" }] : /* istanbul ignore next */ []));
2918
+ options = input.required(/* @ts-ignore */
2919
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
2920
+ /** Contrat Signal Forms. */
2921
+ value = model('', /* @ts-ignore */
2922
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
2923
+ errors = input([], /* @ts-ignore */
2924
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
2925
+ touched = input(false, { ...(ngDevMode ? { debugName: "touched" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2926
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2927
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2928
+ hidden = input(false, { ...(ngDevMode ? { debugName: "hidden" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2929
+ name = input('', /* @ts-ignore */
2930
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
2931
+ touch = output();
2932
+ /** Nom partage par les options : sans lui, chaque radio forme son propre groupe. */
2933
+ nomParDefaut = `st-radio-${(compteur$4 += 1).toString()}`;
2934
+ nomDuGroupe() {
2935
+ return this.name() === '' ? this.nomParDefaut : this.name();
2936
+ }
2937
+ identifiantAide(indice) {
2938
+ return `${this.nomParDefaut}-aide-${indice.toString()}`;
2939
+ }
2940
+ /** Focus sur l'option cochee, sinon la premiere disponible : c'est l'arret du groupe. */
2941
+ focus(options) {
2942
+ const hote = this.element.nativeElement;
2943
+ const cible = hote.querySelector('input:checked') ??
2944
+ hote.querySelector('input:not(:disabled)');
2945
+ cible?.focus(options);
2946
+ }
2947
+ choisir(valeur) {
2948
+ this.value.set(valeur);
2949
+ }
2950
+ aLaSortie(evenement) {
2951
+ const suivant = evenement.relatedTarget;
2952
+ if (suivant !== null && this.element.nativeElement.contains(suivant))
2953
+ return;
2954
+ this.touch.emit();
2955
+ }
2956
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StRadioGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
2957
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StRadioGroup, isStandalone: true, selector: "st-radio-group", inputs: { legende: { classPropertyName: "legende", publicName: "legende", isSignal: true, isRequired: true, transformFunction: null }, aide: { classPropertyName: "aide", publicName: "aide", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { listeners: { "focusout": "aLaSortie($event)" }, properties: { "attr.hidden": "hidden() ? \"\" : null" }, classAttribute: "st-radio-group" }, ngImport: i0, template: "<st-fieldset\n [legende]=\"legende()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n @for (option of options(); track option.valeur; let indice = $index) {\n <div class=\"st-radio-group__option\">\n <input\n class=\"st-radio-group__radio\"\n type=\"radio\"\n [id]=\"nomDuGroupe() + '-' + indice\"\n [name]=\"nomDuGroupe()\"\n [value]=\"option.valeur\"\n [checked]=\"option.valeur === value()\"\n [disabled]=\"disabled() || option.desactivee === true\"\n [attr.aria-describedby]=\"option.aide ? identifiantAide(indice) : null\"\n (change)=\"choisir(option.valeur)\"\n />\n <label class=\"st-radio-group__libelle\" [for]=\"nomDuGroupe() + '-' + indice\">\n {{ option.libelle }}\n </label>\n @if (option.aide; as aide) {\n <p class=\"st-radio-group__aide\" [id]=\"identifiantAide(indice)\">{{ aide }}</p>\n }\n </div>\n }\n</st-fieldset>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-radio-group__option{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:var(--st-espacement-2);align-items:center}.st-radio-group__radio:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-radio-group__radio{inline-size:1.5rem;block-size:1.5rem;margin:0;cursor:pointer;accent-color:var(--st-couleur-primaire)}.st-radio-group__radio:disabled{cursor:not-allowed}.st-radio-group__libelle{cursor:pointer}.st-radio-group__aide{grid-column:2;margin:0;font-size:var(--st-taille-sm);color:var(--st-couleur-texte-discret)}\n"], dependencies: [{ kind: "component", type: StFieldset, selector: "st-fieldset", inputs: ["legende", "aide", "erreurs", "afficherErreurs", "obligatoire"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2958
+ }
2959
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StRadioGroup, decorators: [{
2960
+ type: Component,
2961
+ args: [{ selector: 'st-radio-group', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFieldset], host: {
2962
+ class: 'st-radio-group',
2963
+ '[attr.hidden]': 'hidden() ? "" : null',
2964
+ '(focusout)': 'aLaSortie($event)',
2965
+ }, template: "<st-fieldset\n [legende]=\"legende()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n @for (option of options(); track option.valeur; let indice = $index) {\n <div class=\"st-radio-group__option\">\n <input\n class=\"st-radio-group__radio\"\n type=\"radio\"\n [id]=\"nomDuGroupe() + '-' + indice\"\n [name]=\"nomDuGroupe()\"\n [value]=\"option.valeur\"\n [checked]=\"option.valeur === value()\"\n [disabled]=\"disabled() || option.desactivee === true\"\n [attr.aria-describedby]=\"option.aide ? identifiantAide(indice) : null\"\n (change)=\"choisir(option.valeur)\"\n />\n <label class=\"st-radio-group__libelle\" [for]=\"nomDuGroupe() + '-' + indice\">\n {{ option.libelle }}\n </label>\n @if (option.aide; as aide) {\n <p class=\"st-radio-group__aide\" [id]=\"identifiantAide(indice)\">{{ aide }}</p>\n }\n </div>\n }\n</st-fieldset>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-radio-group__option{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:var(--st-espacement-2);align-items:center}.st-radio-group__radio:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-radio-group__radio{inline-size:1.5rem;block-size:1.5rem;margin:0;cursor:pointer;accent-color:var(--st-couleur-primaire)}.st-radio-group__radio:disabled{cursor:not-allowed}.st-radio-group__libelle{cursor:pointer}.st-radio-group__aide{grid-column:2;margin:0;font-size:var(--st-taille-sm);color:var(--st-couleur-texte-discret)}\n"] }]
2966
+ }], propDecorators: { legende: [{ type: i0.Input, args: [{ isSignal: true, alias: "legende", required: true }] }], aide: [{ type: i0.Input, args: [{ isSignal: true, alias: "aide", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }] } });
2967
+
2968
+ /**
2969
+ * Interrupteur : un reglage qui s'applique IMMEDIATEMENT.
2970
+ *
2971
+ * ```html
2972
+ * <st-switch libelle="Notifications par SMS" [(checked)]="sms" (checkedChange)="enregistrer($event)" />
2973
+ * ```
2974
+ *
2975
+ * ⚠️ **Jamais dans un formulaire a envoyer** (UX-8). Un interrupteur promet que
2976
+ * l'effet a lieu au basculement ; pose au-dessus d'un bouton « Enregistrer », il
2977
+ * ment — la personne croit le reglage applique, quitte la page, et le perd. Pour
2978
+ * un choix envoye plus tard, c'est `st-checkbox`.
2979
+ *
2980
+ * C'est pourquoi il n'affiche aucune erreur de validation : un reglage immediat
2981
+ * ne se valide pas a l'envoi. Un refus du serveur se dit par un message, et
2982
+ * l'interrupteur revient a son etat precedent.
2983
+ *
2984
+ * ⚠️ Une case a cocher native avec `role="switch"` : Espace bascule, l'etiquette
2985
+ * est reliee par `for`, et le lecteur d'ecran annonce « active » ou
2986
+ * « desactive » — mieux qu'un `div` qui imiterait tout cela.
2987
+ */
2988
+ class StSwitch {
2989
+ libelle = input.required(/* @ts-ignore */
2990
+ ...(ngDevMode ? [{ debugName: "libelle" }] : /* istanbul ignore next */ []));
2991
+ aide = input(null, /* @ts-ignore */
2992
+ ...(ngDevMode ? [{ debugName: "aide" }] : /* istanbul ignore next */ []));
2993
+ /** Contrat Signal Forms. */
2994
+ checked = model(false, /* @ts-ignore */
2995
+ ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
2996
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2997
+ hidden = input(false, { ...(ngDevMode ? { debugName: "hidden" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2998
+ name = input('', /* @ts-ignore */
2999
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
3000
+ touch = output();
3001
+ champ = viewChild.required('champ', /* @ts-ignore */
3002
+ ...(ngDevMode ? [{ debugName: "champ" }] : /* istanbul ignore next */ []));
3003
+ focus(options) {
3004
+ this.champ().nativeElement.focus(options);
3005
+ }
3006
+ surChangement(evenement) {
3007
+ this.checked.set(evenement.target.checked);
3008
+ }
3009
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StSwitch, deps: [], target: i0.ɵɵFactoryTarget.Component });
3010
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.6", type: StSwitch, isStandalone: true, selector: "st-switch", inputs: { libelle: { classPropertyName: "libelle", publicName: "libelle", isSignal: true, isRequired: true, transformFunction: null }, aide: { classPropertyName: "aide", publicName: "aide", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", touch: "touch" }, host: { properties: { "attr.hidden": "hidden() ? \"\" : null" }, classAttribute: "st-switch" }, viewQueries: [{ propertyName: "champ", first: true, predicate: ["champ"], descendants: true, isSignal: true }], ngImport: i0, template: "<st-form-field disposition=\"case\" [libelle]=\"libelle()\" [aide]=\"aide()\">\n <input\n #champ\n class=\"st-switch__interrupteur\"\n type=\"checkbox\"\n role=\"switch\"\n stControleDeChamp\n [checked]=\"checked()\"\n [attr.aria-checked]=\"checked()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n (change)=\"surChangement($event)\"\n (blur)=\"touch.emit()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-switch__interrupteur:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-switch__interrupteur{position:relative;inline-size:2.75rem;block-size:1.5rem;margin:0;cursor:pointer;background:var(--st-couleur-surface-enfoncee);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-pleine);appearance:none}.st-switch__interrupteur:after{position:absolute;inset-block-start:50%;inset-inline-start:.125rem;inline-size:1.125rem;block-size:1.125rem;content:\"\";background:var(--st-couleur-texte-discret);border-radius:var(--st-rayon-pleine);transition:inset-inline-start var(--st-duree-rapide) ease;transform:translateY(-50%)}.st-switch__interrupteur:checked{background:var(--st-couleur-primaire);border-color:var(--st-couleur-primaire)}.st-switch__interrupteur:checked:after{inset-inline-start:calc(100% - 1.25rem);background:var(--st-couleur-sur-primaire)}.st-switch__interrupteur:disabled{cursor:not-allowed;opacity:.6}@media(forced-colors:active){.st-switch__interrupteur{border-color:CanvasText}.st-switch__interrupteur:after{background:CanvasText}.st-switch__interrupteur:checked:after{background:Highlight}}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3011
+ }
3012
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StSwitch, decorators: [{
3013
+ type: Component,
3014
+ args: [{ selector: 'st-switch', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp], host: {
3015
+ class: 'st-switch',
3016
+ '[attr.hidden]': 'hidden() ? "" : null',
3017
+ }, template: "<st-form-field disposition=\"case\" [libelle]=\"libelle()\" [aide]=\"aide()\">\n <input\n #champ\n class=\"st-switch__interrupteur\"\n type=\"checkbox\"\n role=\"switch\"\n stControleDeChamp\n [checked]=\"checked()\"\n [attr.aria-checked]=\"checked()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n (change)=\"surChangement($event)\"\n (blur)=\"touch.emit()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-switch__interrupteur:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-switch__interrupteur{position:relative;inline-size:2.75rem;block-size:1.5rem;margin:0;cursor:pointer;background:var(--st-couleur-surface-enfoncee);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-pleine);appearance:none}.st-switch__interrupteur:after{position:absolute;inset-block-start:50%;inset-inline-start:.125rem;inline-size:1.125rem;block-size:1.125rem;content:\"\";background:var(--st-couleur-texte-discret);border-radius:var(--st-rayon-pleine);transition:inset-inline-start var(--st-duree-rapide) ease;transform:translateY(-50%)}.st-switch__interrupteur:checked{background:var(--st-couleur-primaire);border-color:var(--st-couleur-primaire)}.st-switch__interrupteur:checked:after{inset-inline-start:calc(100% - 1.25rem);background:var(--st-couleur-sur-primaire)}.st-switch__interrupteur:disabled{cursor:not-allowed;opacity:.6}@media(forced-colors:active){.st-switch__interrupteur{border-color:CanvasText}.st-switch__interrupteur:after{background:CanvasText}.st-switch__interrupteur:checked:after{background:Highlight}}\n"] }]
3018
+ }], propDecorators: { libelle: [{ type: i0.Input, args: [{ isSignal: true, alias: "libelle", required: true }] }], aide: [{ type: i0.Input, args: [{ isSignal: true, alias: "aide", required: false }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }], champ: [{ type: i0.ViewChild, args: ['champ', { isSignal: true }] }] } });
3019
+
3020
+ let compteur$3 = 0;
3021
+ /**
3022
+ * Recapitulatif des erreurs d'un formulaire, a l'envoi.
3023
+ *
3024
+ * ```html
3025
+ * <st-error-summary [erreurs]="tentative() ? virement().errorSummary() : []" />
3026
+ * ```
3027
+ *
3028
+ * ⚠️ **Il prend le focus en apparaissant** (UX-5). Sans lui, une personne au
3029
+ * clavier ou au lecteur d'ecran appuie sur « Envoyer », et rien ne se passe : le
3030
+ * champ refuse est en haut de la page, hors de sa vue, et personne ne le lui dit.
3031
+ *
3032
+ * ⚠️ **Chaque ligne nomme son champ.** « Ce champ est obligatoire. » repete cinq
3033
+ * fois n'apprend rien ; « Nom du beneficiaire : ce champ est obligatoire. », si.
3034
+ * Le libelle est lu sur l'element lie au champ, par Signal Forms — sans registre,
3035
+ * et quel que soit le composant.
3036
+ *
3037
+ * ⚠️ **Une ligne par champ**, la premiere erreur seulement : trois erreurs sur le
3038
+ * meme IBAN font trois lignes pour une seule correction.
3039
+ *
3040
+ * ⚠️ **Une region nommee par son titre.** Un `div` sans role ne PEUT pas porter
3041
+ * `aria-labelledby` : le nom serait ignore, et le focus n'annoncerait rien. En
3042
+ * region, il annonce « 2 erreurs a corriger » a l'arrivee du focus, et se
3043
+ * retrouve dans la liste des reperes.
3044
+ *
3045
+ * Chaque ligne amene le focus sur son champ : c'est le champ, et non la ligne,
3046
+ * qu'il faut corriger. Une erreur rattachee au formulaire entier — un refus du
3047
+ * serveur sur un champ inconnu — s'affiche sans lien.
3048
+ */
3049
+ class StErrorSummary {
3050
+ libelles = inject(ST_LIBELLES);
3051
+ locale = inject(ST_LOCALE);
3052
+ element = inject(ElementRef);
3053
+ injecteur = inject(Injector);
3054
+ erreurs = input.required(/* @ts-ignore */
3055
+ ...(ngDevMode ? [{ debugName: "erreurs" }] : /* istanbul ignore next */ []));
3056
+ /** `null` : « N erreurs a corriger », du dictionnaire. */
3057
+ titre = input(null, /* @ts-ignore */
3058
+ ...(ngDevMode ? [{ debugName: "titre" }] : /* istanbul ignore next */ []));
3059
+ /** Unique par instance : deux recapitulatifs sur une page partageraient leur titre. */
3060
+ identifiantDuTitre = `st-recapitulatif-${(compteur$3 += 1).toString()}`;
3061
+ lignes = computed(() => {
3062
+ const libelles = this.libelles();
3063
+ const locale = this.locale();
3064
+ const vus = new Set();
3065
+ const lignes = [];
3066
+ for (const erreur of this.erreurs()) {
3067
+ const champ = erreur.fieldTree;
3068
+ if (champ !== undefined) {
3069
+ if (vus.has(champ))
3070
+ continue;
3071
+ vus.add(champ);
3072
+ }
3073
+ const etat = champ?.();
3074
+ const liaison = etat?.formFieldBindings()[0];
3075
+ lignes.push({
3076
+ libelle: liaison === undefined ? null : libelleDe(liaison.element),
3077
+ message: messageDErreur(erreur, libelles, locale),
3078
+ aller: () => {
3079
+ etat?.focusBoundControl();
3080
+ },
3081
+ });
3082
+ }
3083
+ return lignes;
3084
+ }, /* @ts-ignore */
3085
+ ...(ngDevMode ? [{ debugName: "lignes" }] : /* istanbul ignore next */ []));
3086
+ titreLu = computed(() => this.titre() ?? this.libelles().erreursACorriger(this.lignes().length), /* @ts-ignore */
3087
+ ...(ngDevMode ? [{ debugName: "titreLu" }] : /* istanbul ignore next */ []));
3088
+ constructor() {
3089
+ // Le focus a l'APPARITION seulement : le reprendre a chaque correction
3090
+ // arracherait la personne au champ qu'elle est en train de corriger.
3091
+ let visible = false;
3092
+ effect(() => {
3093
+ const maintenant = this.lignes().length > 0;
3094
+ if (maintenant && !visible) {
3095
+ afterNextRender(() => {
3096
+ this.focus();
3097
+ }, { injector: this.injecteur });
3098
+ }
3099
+ visible = maintenant;
3100
+ });
3101
+ }
3102
+ /** A appeler apres un envoi refuse, quand le recapitulatif etait deja visible. */
3103
+ focus() {
3104
+ this.element.nativeElement.querySelector('.st-error-summary__cadre')?.focus();
3105
+ }
3106
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StErrorSummary, deps: [], target: i0.ɵɵFactoryTarget.Component });
3107
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StErrorSummary, isStandalone: true, selector: "st-error-summary", inputs: { erreurs: { classPropertyName: "erreurs", publicName: "erreurs", isSignal: true, isRequired: true, transformFunction: null }, titre: { classPropertyName: "titre", publicName: "titre", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.hidden": "lignes().length === 0 ? \"\" : null" }, classAttribute: "st-error-summary" }, ngImport: i0, template: "@if (lignes().length > 0) {\n <div\n class=\"st-error-summary__cadre\"\n role=\"region\"\n tabindex=\"-1\"\n [attr.aria-labelledby]=\"identifiantDuTitre\"\n >\n <h2 class=\"st-error-summary__titre\" [id]=\"identifiantDuTitre\">\n <st-icon nom=\"cercle-croix\" taille=\"sm\" />\n {{ titreLu() }}\n </h2>\n <ul class=\"st-error-summary__liste\">\n @for (ligne of lignes(); track $index) {\n <li>\n @if (ligne.libelle !== null) {\n <button type=\"button\" class=\"st-error-summary__lien\" (click)=\"ligne.aller()\">\n {{ ligne.libelle }} : {{ ligne.message }}\n </button>\n } @else {\n {{ ligne.message }}\n }\n </li>\n }\n </ul>\n </div>\n}\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block;margin-block-end:var(--st-espacement-5)}.st-error-summary__cadre:focus{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-error-summary__cadre{padding:var(--st-espacement-4);color:var(--st-couleur-sur-danger-discret);background:var(--st-couleur-danger-discret);border:2px solid var(--st-couleur-danger);border-radius:var(--st-rayon-md)}.st-error-summary__titre{display:flex;gap:var(--st-espacement-2);align-items:center;margin:0 0 var(--st-espacement-2);font-size:var(--st-taille-lg)}.st-error-summary__liste{padding-inline-start:var(--st-espacement-5);margin:0}.st-error-summary__lien:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-error-summary__lien{padding:0;font:inherit;color:inherit;text-align:start;text-decoration:underline;cursor:pointer;background:none;border:0}@media(forced-colors:active){.st-error-summary__cadre{border-color:CanvasText}}\n"], dependencies: [{ kind: "component", type: StIcon, selector: "st-icon", inputs: ["nom", "taille", "ton", "etiquette"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3108
+ }
3109
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StErrorSummary, decorators: [{
3110
+ type: Component,
3111
+ args: [{ selector: 'st-error-summary', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StIcon], host: {
3112
+ class: 'st-error-summary',
3113
+ '[attr.hidden]': 'lignes().length === 0 ? "" : null',
3114
+ }, template: "@if (lignes().length > 0) {\n <div\n class=\"st-error-summary__cadre\"\n role=\"region\"\n tabindex=\"-1\"\n [attr.aria-labelledby]=\"identifiantDuTitre\"\n >\n <h2 class=\"st-error-summary__titre\" [id]=\"identifiantDuTitre\">\n <st-icon nom=\"cercle-croix\" taille=\"sm\" />\n {{ titreLu() }}\n </h2>\n <ul class=\"st-error-summary__liste\">\n @for (ligne of lignes(); track $index) {\n <li>\n @if (ligne.libelle !== null) {\n <button type=\"button\" class=\"st-error-summary__lien\" (click)=\"ligne.aller()\">\n {{ ligne.libelle }} : {{ ligne.message }}\n </button>\n } @else {\n {{ ligne.message }}\n }\n </li>\n }\n </ul>\n </div>\n}\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block;margin-block-end:var(--st-espacement-5)}.st-error-summary__cadre:focus{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-error-summary__cadre{padding:var(--st-espacement-4);color:var(--st-couleur-sur-danger-discret);background:var(--st-couleur-danger-discret);border:2px solid var(--st-couleur-danger);border-radius:var(--st-rayon-md)}.st-error-summary__titre{display:flex;gap:var(--st-espacement-2);align-items:center;margin:0 0 var(--st-espacement-2);font-size:var(--st-taille-lg)}.st-error-summary__liste{padding-inline-start:var(--st-espacement-5);margin:0}.st-error-summary__lien:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-error-summary__lien{padding:0;font:inherit;color:inherit;text-align:start;text-decoration:underline;cursor:pointer;background:none;border:0}@media(forced-colors:active){.st-error-summary__cadre{border-color:CanvasText}}\n"] }]
3115
+ }], ctorParameters: () => [], propDecorators: { erreurs: [{ type: i0.Input, args: [{ isSignal: true, alias: "erreurs", required: true }] }], titre: [{ type: i0.Input, args: [{ isSignal: true, alias: "titre", required: false }] }] } });
3116
+ /**
3117
+ * Libelle d'un champ, lu sur l'element lie : l'attribut pose par le socle, sinon
3118
+ * l'etiquette native, sinon `aria-label`.
3119
+ */
3120
+ function libelleDe(element) {
3121
+ const socle = element.querySelector('[data-libelle]')?.getAttribute('data-libelle');
3122
+ if (socle !== null && socle !== undefined)
3123
+ return socle;
3124
+ const etiquette = element.labels?.[0]?.textContent.trim();
3125
+ if (etiquette !== undefined && etiquette !== '')
3126
+ return etiquette;
3127
+ return element.getAttribute('aria-label');
3128
+ }
3129
+
3130
+ /**
3131
+ * Mot de passe, avec revelation.
3132
+ *
3133
+ * ```html
3134
+ * <st-password-field libelle="Mot de passe" usage="creation" [regles]="regles" [formField]="compte.motDePasse" />
3135
+ * ```
3136
+ *
3137
+ * ⚠️ **`autocomplete` selon l'usage** (SEC-2) : `current-password` a la connexion,
3138
+ * `new-password` a la creation — c'est ce qui fait proposer par le gestionnaire
3139
+ * un mot de passe FORT au bon endroit, et non l'ancien.
3140
+ *
3141
+ * ⚠️ **Le collage n'est jamais bloque** (SEC-1, NIST SP 800-63B) : l'interdire
3142
+ * pousse vers des mots de passe courts, retenus de tete, et casse les
3143
+ * gestionnaires de mots de passe.
3144
+ *
3145
+ * ⚠️ **Revele, il se remasque des que le focus quitte le champ, et a l'envoi du
3146
+ * formulaire** (SEC-4) : un mot de passe laisse en clair reste lisible par-dessus
3147
+ * l'epaule, sur l'ecran suivant comme sur celui-ci. L'envoi par Entree ne fait
3148
+ * pas quitter le champ : d'ou l'ecoute de `submit`, qui remonte jusqu'au document.
3149
+ *
3150
+ * Le bouton de revelation est un bouton bascule : `aria-pressed` porte l'etat, et
3151
+ * l'etiquette ne change PAS — « Masquer le mot de passe, enfonce » se lirait
3152
+ * comme une double negation.
3153
+ *
3154
+ * ⚠️ **Aucun indicateur de « force »**, et c'est un refus. Une estimation faite
3155
+ * dans le navigateur donne « fort » a `Motdepasse1!` : elle rassure a tort. Les
3156
+ * regles sont affichees AVANT la saisie (UX-10), et c'est le serveur qui verifie —
3157
+ * y compris contre les mots de passe deja divulgues, qu'aucune regle locale ne
3158
+ * voit.
3159
+ */
3160
+ class StPasswordField extends ControleDeSaisie {
3161
+ libelles = inject(ST_LIBELLES);
3162
+ element = inject(ElementRef);
3163
+ usage = input('connexion', /* @ts-ignore */
3164
+ ...(ngDevMode ? [{ debugName: "usage" }] : /* istanbul ignore next */ []));
3165
+ /** Regles affichees avant la saisie, comme aide du champ. */
3166
+ regles = input([], /* @ts-ignore */
3167
+ ...(ngDevMode ? [{ debugName: "regles" }] : /* istanbul ignore next */ []));
3168
+ revele = signal(false, /* @ts-ignore */
3169
+ ...(ngDevMode ? [{ debugName: "revele" }] : /* istanbul ignore next */ []));
3170
+ autocomplete = computed(() => this.usage() === 'creation' ? 'new-password' : 'current-password', /* @ts-ignore */
3171
+ ...(ngDevMode ? [{ debugName: "autocomplete" }] : /* istanbul ignore next */ []));
3172
+ aideLue = computed(() => {
3173
+ const regles = this.regles();
3174
+ const aide = this.aide();
3175
+ if (regles.length === 0)
3176
+ return aide;
3177
+ return [aide, ...regles].filter((texte) => texte !== null).join(' ');
3178
+ }, /* @ts-ignore */
3179
+ ...(ngDevMode ? [{ debugName: "aideLue" }] : /* istanbul ignore next */ []));
3180
+ basculer() {
3181
+ this.revele.update((revele) => !revele);
3182
+ // Revele alors que le focus est ailleurs — clic direct sur le bouton, champ
3183
+ // rempli par le gestionnaire —, il ne pourrait plus quitter le champ : rien ne
3184
+ // le remasquerait. Au clavier, le focus est deja sur le bouton, et y reste.
3185
+ const hote = this.element.nativeElement;
3186
+ if (this.revele() && !hote.contains(hote.ownerDocument.activeElement))
3187
+ this.focus();
3188
+ }
3189
+ surSaisie(evenement) {
3190
+ this.value.set(this.texteSaisi(evenement));
3191
+ }
3192
+ aLEnvoi(evenement) {
3193
+ const formulaire = evenement.target;
3194
+ if (formulaire?.contains(this.element.nativeElement) === true)
3195
+ this.revele.set(false);
3196
+ }
3197
+ aLaSortie(evenement) {
3198
+ const suivant = evenement.relatedTarget;
3199
+ if (suivant !== null && this.element.nativeElement.contains(suivant))
3200
+ return;
3201
+ this.revele.set(false);
3202
+ }
3203
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPasswordField, deps: null, target: i0.ɵɵFactoryTarget.Component });
3204
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StPasswordField, isStandalone: true, selector: "st-password-field", inputs: { usage: { classPropertyName: "usage", publicName: "usage", isSignal: true, isRequired: false, transformFunction: null }, regles: { classPropertyName: "regles", publicName: "regles", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "focusout": "aLaSortie($event)", "document:submit": "aLEnvoi($event)" }, classAttribute: "st-password-field" }, usesInheritance: true, ngImport: i0, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <div class=\"st-password-field__ligne\">\n <input\n #champ\n class=\"st-password-field__champ\"\n stControleDeChamp\n spellcheck=\"false\"\n autocapitalize=\"none\"\n autocorrect=\"off\"\n [type]=\"revele() ? 'text' : 'password'\"\n [value]=\"value()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"touch.emit()\"\n />\n <!-- eslint-disable-next-line @angular-eslint/template/elements-content --\n Le contenu vient du gabarit de stIconButton. Le nom accessible est\n impose par le type (`etiquette` est requis) et verifie par axe.\n `mousedown` sans effet par defaut : le focus reste dans le champ. Safari\n ne donne pas le focus a un bouton clique, et la sortie du champ\n remasquerait juste avant que le clic ne revele a nouveau. -->\n <button\n stIconButton\n type=\"button\"\n [icone]=\"revele() ? 'oeil-barre' : 'oeil'\"\n [etiquette]=\"libelles().afficherLeMotDePasse\"\n [enfonce]=\"revele()\"\n [disabled]=\"disabled()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"basculer()\"\n ></button>\n </div>\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-password-field__ligne{display:flex;gap:var(--st-espacement-1);align-items:center}.st-password-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-password-field__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-password-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-password-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-password-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-password-field__champ{flex:1 1 auto}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }, { kind: "component", type: StIconButton, selector: "button[stIconButton]", inputs: ["icone", "etiquette", "variante", "taille", "enfonce"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3205
+ }
3206
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPasswordField, decorators: [{
3207
+ type: Component,
3208
+ args: [{ selector: 'st-password-field', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp, StIconButton], host: {
3209
+ class: 'st-password-field',
3210
+ '(focusout)': 'aLaSortie($event)',
3211
+ '(document:submit)': 'aLEnvoi($event)',
3212
+ }, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <div class=\"st-password-field__ligne\">\n <input\n #champ\n class=\"st-password-field__champ\"\n stControleDeChamp\n spellcheck=\"false\"\n autocapitalize=\"none\"\n autocorrect=\"off\"\n [type]=\"revele() ? 'text' : 'password'\"\n [value]=\"value()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"touch.emit()\"\n />\n <!-- eslint-disable-next-line @angular-eslint/template/elements-content --\n Le contenu vient du gabarit de stIconButton. Le nom accessible est\n impose par le type (`etiquette` est requis) et verifie par axe.\n `mousedown` sans effet par defaut : le focus reste dans le champ. Safari\n ne donne pas le focus a un bouton clique, et la sortie du champ\n remasquerait juste avant que le clic ne revele a nouveau. -->\n <button\n stIconButton\n type=\"button\"\n [icone]=\"revele() ? 'oeil-barre' : 'oeil'\"\n [etiquette]=\"libelles().afficherLeMotDePasse\"\n [enfonce]=\"revele()\"\n [disabled]=\"disabled()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"basculer()\"\n ></button>\n </div>\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-password-field__ligne{display:flex;gap:var(--st-espacement-1);align-items:center}.st-password-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-password-field__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-password-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-password-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-password-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-password-field__champ{flex:1 1 auto}\n"] }]
3213
+ }], propDecorators: { usage: [{ type: i0.Input, args: [{ isSignal: true, alias: "usage", required: false }] }], regles: [{ type: i0.Input, args: [{ isSignal: true, alias: "regles", required: false }] }] } });
3214
+
3215
+ /**
3216
+ * Code a usage unique : second facteur, code recu par SMS ou par courriel.
3217
+ *
3218
+ * ⚠️ **UN seul champ, pas six cases** (UX-9). Six champs cassent le collage, le
3219
+ * remplissage automatique du code recu par SMS, et la lecture au lecteur d'ecran,
3220
+ * qui annonce six champs sans nom au lieu d'un seul.
3221
+ *
3222
+ * ⚠️ `autocomplete="one-time-code"` : iOS et Android proposent alors le code recu,
3223
+ * d'un toucher. Clavier numerique par `inputmode`, et NON par `type="number"`,
3224
+ * qui retire les zeros de tete et accepte « 1e5 ».
3225
+ *
3226
+ * ⚠️ **Aucun envoi automatique** a la sixieme saisie : un formulaire qui part de
3227
+ * lui-meme surprend (WCAG 3.2.2), et part aussi avec un code mal colle. Le code
3228
+ * colle est nettoye de ses espaces et tirets — « 123 456 » —, jamais tronque.
3229
+ */
3230
+ class StOtpField extends ControleDeSaisie {
3231
+ libelles = inject(ST_LIBELLES);
3232
+ longueur = input(6, /* @ts-ignore */
3233
+ ...(ngDevMode ? [{ debugName: "longueur" }] : /* istanbul ignore next */ []));
3234
+ aideLue = computed(() => this.aide() ?? this.libelles().codeAChiffres(this.longueur()), /* @ts-ignore */
3235
+ ...(ngDevMode ? [{ debugName: "aideLue" }] : /* istanbul ignore next */ []));
3236
+ surSaisie(evenement) {
3237
+ this.value.set(this.texteSaisi(evenement).replace(/[\s-]/g, ''));
3238
+ }
3239
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StOtpField, deps: null, target: i0.ɵɵFactoryTarget.Component });
3240
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StOtpField, isStandalone: true, selector: "st-otp-field", inputs: { longueur: { classPropertyName: "longueur", publicName: "longueur", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "st-otp-field" }, usesInheritance: true, ngImport: i0, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <input\n #champ\n class=\"st-otp-field__champ\"\n stControleDeChamp\n type=\"text\"\n inputmode=\"numeric\"\n autocomplete=\"one-time-code\"\n spellcheck=\"false\"\n autocapitalize=\"none\"\n [value]=\"value()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"touch.emit()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-otp-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-otp-field__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-otp-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-otp-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-otp-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-otp-field__champ{max-inline-size:12rem;font-family:var(--st-police-mono);font-variant-numeric:tabular-nums;letter-spacing:.3em}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3241
+ }
3242
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StOtpField, decorators: [{
3243
+ type: Component,
3244
+ args: [{ selector: 'st-otp-field', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp], host: { class: 'st-otp-field' }, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <input\n #champ\n class=\"st-otp-field__champ\"\n stControleDeChamp\n type=\"text\"\n inputmode=\"numeric\"\n autocomplete=\"one-time-code\"\n spellcheck=\"false\"\n autocapitalize=\"none\"\n [value]=\"value()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"touch.emit()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-otp-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-otp-field__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-otp-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-otp-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-otp-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-otp-field__champ{max-inline-size:12rem;font-family:var(--st-police-mono);font-variant-numeric:tabular-nums;letter-spacing:.3em}\n"] }]
3245
+ }], propDecorators: { longueur: [{ type: i0.Input, args: [{ isSignal: true, alias: "longueur", required: false }] }] } });
3246
+
3247
+ /**
3248
+ * Champ de recherche : un repere, un champ, un bouton.
3249
+ *
3250
+ * ```html
3251
+ * <st-search-field libelle="Rechercher un beneficiaire" (recherche)="filtrer($event)" />
3252
+ * ```
3253
+ *
3254
+ * ⚠️ **Un repere `search`** : la recherche se retrouve dans la liste des reperes du
3255
+ * lecteur d'ecran, a cote de la navigation et du contenu.
3256
+ *
3257
+ * L'etiquette est lue sans etre affichee — seule exception au socle (UX-1) : la
3258
+ * loupe, le bouton et la position disent deja le but du champ.
3259
+ *
3260
+ * ⚠️ **Jamais dans l'adresse quand le terme est personnel** (SEC-6). Chercher un
3261
+ * nom, un IBAN ou un numero en `?q=` inscrit la donnee dans l'historique, dans les
3262
+ * journaux des intermediaires, et dans l'en-tete `Referer` des pages suivantes.
3263
+ * `recherche` emet le terme : c'est a l'appelant de ne pas le mettre dans l'URL.
3264
+ */
3265
+ class StSearchField extends ControleDeSaisie {
3266
+ libelles = inject(ST_LIBELLES);
3267
+ /** Emis a la validation — Entree ou bouton —, et non a chaque frappe. */
3268
+ recherche = output();
3269
+ surSaisie(evenement) {
3270
+ this.value.set(this.texteSaisi(evenement));
3271
+ }
3272
+ lancer(evenement) {
3273
+ evenement?.preventDefault();
3274
+ this.recherche.emit(this.value().trim());
3275
+ }
3276
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StSearchField, deps: null, target: i0.ɵɵFactoryTarget.Component });
3277
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: StSearchField, isStandalone: true, selector: "st-search-field", outputs: { recherche: "recherche" }, host: { attributes: { "role": "search" }, classAttribute: "st-search-field" }, usesInheritance: true, ngImport: i0, template: "<st-form-field\n libelleMasque\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n>\n <div class=\"st-search-field__ligne\">\n <st-icon class=\"st-search-field__loupe\" nom=\"recherche\" taille=\"sm\" />\n <input\n #champ\n class=\"st-search-field__champ\"\n stControleDeChamp\n type=\"search\"\n enterkeyhint=\"search\"\n [value]=\"value()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n (input)=\"surSaisie($event)\"\n (keydown.enter)=\"lancer($event)\"\n (blur)=\"touch.emit()\"\n />\n <button stButton type=\"button\" [disabled]=\"disabled()\" (click)=\"lancer()\">\n {{ libelles().rechercher }}\n </button>\n </div>\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-search-field__ligne{position:relative;display:flex;gap:var(--st-espacement-2);align-items:center}.st-search-field__loupe{position:absolute;inset-inline-start:var(--st-espacement-3);color:var(--st-couleur-texte-discret);pointer-events:none}.st-search-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-search-field__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-search-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-search-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-search-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-search-field__champ{flex:1 1 auto;padding-inline-start:calc(var(--st-espacement-3) * 2 + 1rem)}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }, { kind: "component", type: StIcon, selector: "st-icon", inputs: ["nom", "taille", "ton", "etiquette"] }, { kind: "component", type: StButton, selector: "button[stButton]", inputs: ["variante", "taille", "pleineLargeur", "iconeAvant", "iconeApres", "chargement", "indisponible"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3278
+ }
3279
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StSearchField, decorators: [{
3280
+ type: Component,
3281
+ args: [{ selector: 'st-search-field', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp, StIcon, StButton], host: { class: 'st-search-field', role: 'search' }, template: "<st-form-field\n libelleMasque\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n>\n <div class=\"st-search-field__ligne\">\n <st-icon class=\"st-search-field__loupe\" nom=\"recherche\" taille=\"sm\" />\n <input\n #champ\n class=\"st-search-field__champ\"\n stControleDeChamp\n type=\"search\"\n enterkeyhint=\"search\"\n [value]=\"value()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n (input)=\"surSaisie($event)\"\n (keydown.enter)=\"lancer($event)\"\n (blur)=\"touch.emit()\"\n />\n <button stButton type=\"button\" [disabled]=\"disabled()\" (click)=\"lancer()\">\n {{ libelles().rechercher }}\n </button>\n </div>\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-search-field__ligne{position:relative;display:flex;gap:var(--st-espacement-2);align-items:center}.st-search-field__loupe{position:absolute;inset-inline-start:var(--st-espacement-3);color:var(--st-couleur-texte-discret);pointer-events:none}.st-search-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-search-field__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-search-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-search-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-search-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-search-field__champ{flex:1 1 auto;padding-inline-start:calc(var(--st-espacement-3) * 2 + 1rem)}\n"] }]
3282
+ }], propDecorators: { recherche: [{ type: i0.Output, args: ["recherche"] }] } });
3283
+
3284
+ /**
3285
+ * IBAN. La valeur du modele est NORMALISEE — sans espaces, en majuscules —, et
3286
+ * l'affichage groupe par quatre.
3287
+ *
3288
+ * ```html
3289
+ * <st-iban-field libelle="IBAN du beneficiaire" [formField]="virement.iban" />
3290
+ * <!-- schema : validate(chemin.iban, ({ value }) => erreurIban(value())) -->
3291
+ * ```
3292
+ *
3293
+ * ⚠️ **Le groupement se fait a la sortie du champ, jamais pendant la frappe.**
3294
+ * Reformater a chaque caractere deplace le curseur a la fin : corriger une
3295
+ * erreur au milieu de 27 caracteres devient impossible, chaque frappe renvoyant a
3296
+ * la fin de la ligne.
3297
+ *
3298
+ * ⚠️ **Aucune verification de validite ici** : elle appartient au schema, par
3299
+ * `erreurIban`, pour s'afficher au bon moment et par le bon canal. Et elle reste un
3300
+ * confort (SEC-5) : le serveur revalide.
3301
+ *
3302
+ * `autocapitalize="characters"` et correction orthographique coupee : un IBAN
3303
+ * n'est pas un mot, et une correction automatique le fausserait.
3304
+ */
3305
+ class StIbanField extends ControleDeSaisie {
3306
+ libelles = inject(ST_LIBELLES);
3307
+ enSaisie = signal(false, /* @ts-ignore */
3308
+ ...(ngDevMode ? [{ debugName: "enSaisie" }] : /* istanbul ignore next */ []));
3309
+ brut = signal('', /* @ts-ignore */
3310
+ ...(ngDevMode ? [{ debugName: "brut" }] : /* istanbul ignore next */ []));
3311
+ aideLue = computed(() => this.aide() ?? this.libelles().exempleDIban, /* @ts-ignore */
3312
+ ...(ngDevMode ? [{ debugName: "aideLue" }] : /* istanbul ignore next */ []));
3313
+ /**
3314
+ * Brut pendant la saisie ; groupe par quatre au repos. Une valeur changee de
3315
+ * l'exterieur pendant la saisie — remise a zero du formulaire — l'emporte sur
3316
+ * le brut, qui ne la represente plus.
3317
+ */
3318
+ affichage = computed(() => {
3319
+ const valeur = this.value();
3320
+ const brut = this.brut();
3321
+ return this.enSaisie() && normaliserIban(brut) === valeur ? brut : formaterIban(valeur);
3322
+ }, /* @ts-ignore */
3323
+ ...(ngDevMode ? [{ debugName: "affichage" }] : /* istanbul ignore next */ []));
3324
+ auFocus() {
3325
+ this.brut.set(formaterIban(this.value()));
3326
+ this.enSaisie.set(true);
3327
+ }
3328
+ surSaisie(evenement) {
3329
+ const saisie = this.texteSaisi(evenement);
3330
+ this.brut.set(saisie);
3331
+ this.value.set(normaliserIban(saisie));
3332
+ }
3333
+ aLaSortie() {
3334
+ this.enSaisie.set(false);
3335
+ this.touch.emit();
3336
+ }
3337
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StIbanField, deps: null, target: i0.ɵɵFactoryTarget.Component });
3338
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: StIbanField, isStandalone: true, selector: "st-iban-field", host: { classAttribute: "st-iban-field" }, usesInheritance: true, ngImport: i0, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <input\n #champ\n class=\"st-iban-field__champ\"\n stControleDeChamp\n type=\"text\"\n autocomplete=\"off\"\n autocapitalize=\"characters\"\n autocorrect=\"off\"\n spellcheck=\"false\"\n [value]=\"affichage()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (focus)=\"auFocus()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"aLaSortie()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-iban-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-iban-field__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-iban-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-iban-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-iban-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-iban-field__champ{font-family:var(--st-police-mono);font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3339
+ }
3340
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StIbanField, decorators: [{
3341
+ type: Component,
3342
+ args: [{ selector: 'st-iban-field', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp], host: { class: 'st-iban-field' }, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <input\n #champ\n class=\"st-iban-field__champ\"\n stControleDeChamp\n type=\"text\"\n autocomplete=\"off\"\n autocapitalize=\"characters\"\n autocorrect=\"off\"\n spellcheck=\"false\"\n [value]=\"affichage()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (focus)=\"auFocus()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"aLaSortie()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-iban-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-iban-field__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-iban-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-iban-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-iban-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-iban-field__champ{font-family:var(--st-police-mono);font-variant-numeric:tabular-nums}\n"] }]
3343
+ }] });
3344
+
3345
+ /**
3346
+ * Numero de telephone. La valeur du modele ne garde que les chiffres et le `+` de
3347
+ * tete ; la saisie s'affiche telle qu'elle a ete tapee.
3348
+ *
3349
+ * ```html
3350
+ * <st-phone-field libelle="Telephone portable" [formField]="contact.telephone" />
3351
+ * <!-- schema : validate(chemin.telephone, ({ value }) => erreurTelephone(value())) -->
3352
+ * ```
3353
+ *
3354
+ * ⚠️ **Aucun formatage automatique.** Le groupement des chiffres depend du pays,
3355
+ * et reformater pendant la frappe renvoie le curseur en fin de ligne. La saisie
3356
+ * reste celle de la personne, espaces et points compris.
3357
+ *
3358
+ * ⚠️ **Pas de selecteur de pays, pas de verification par pays.** Il faudrait une
3359
+ * table des plans de numerotation, lourde et souvent perimee : c'est au serveur
3360
+ * de dire si le numero existe (SEC-5). `erreurTelephone` ne verifie que la forme.
3361
+ *
3362
+ * `type="tel"`, et jamais `type="number"` : un numero n'est pas une quantite, et le
3363
+ * champ numerique retire le zero de tete et le `+`.
3364
+ */
3365
+ class StPhoneField extends ControleDeSaisie {
3366
+ autocomplete = input('tel', /* @ts-ignore */
3367
+ ...(ngDevMode ? [{ debugName: "autocomplete" }] : /* istanbul ignore next */ []));
3368
+ brut = signal(null, /* @ts-ignore */
3369
+ ...(ngDevMode ? [{ debugName: "brut" }] : /* istanbul ignore next */ []));
3370
+ /**
3371
+ * La saisie telle que tapee, tant qu'elle represente encore la valeur ; une
3372
+ * valeur changee de l'exterieur — remise a zero, pre-remplissage — l'emporte.
3373
+ */
3374
+ affichage = computed(() => {
3375
+ const valeur = this.value();
3376
+ const brut = this.brut();
3377
+ return brut !== null && normaliserTelephone(brut) === valeur ? brut : valeur;
3378
+ }, /* @ts-ignore */
3379
+ ...(ngDevMode ? [{ debugName: "affichage" }] : /* istanbul ignore next */ []));
3380
+ surSaisie(evenement) {
3381
+ const saisie = this.texteSaisi(evenement);
3382
+ this.brut.set(saisie);
3383
+ this.value.set(normaliserTelephone(saisie));
3384
+ }
3385
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPhoneField, deps: null, target: i0.ɵɵFactoryTarget.Component });
3386
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StPhoneField, isStandalone: true, selector: "st-phone-field", inputs: { autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "st-phone-field" }, usesInheritance: true, ngImport: i0, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <input\n #champ\n class=\"st-phone-field__champ\"\n stControleDeChamp\n type=\"tel\"\n [attr.autocomplete]=\"autocomplete()\"\n [value]=\"affichage()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"touch.emit()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-phone-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-phone-field__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-phone-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-phone-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-phone-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-phone-field__champ{font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3387
+ }
3388
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPhoneField, decorators: [{
3389
+ type: Component,
3390
+ args: [{ selector: 'st-phone-field', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp], host: { class: 'st-phone-field' }, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <input\n #champ\n class=\"st-phone-field__champ\"\n stControleDeChamp\n type=\"tel\"\n [attr.autocomplete]=\"autocomplete()\"\n [value]=\"affichage()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"touch.emit()\"\n />\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-phone-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-phone-field__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-phone-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-phone-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-phone-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-phone-field__champ{font-variant-numeric:tabular-nums}\n"] }]
3391
+ }], propDecorators: { autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }] } });
3392
+
3393
+ /** Sans accents ni casse : « eleve » trouve « Élève ». */
3394
+ function normaliserPourRecherche(texte) {
3395
+ return texte
3396
+ .normalize('NFD')
3397
+ .replace(/[\u0300-\u036f]/g, '')
3398
+ .toLocaleLowerCase()
3399
+ .trim();
3400
+ }
3401
+ /** Options dont le libelle CONTIENT le texte cherche, sans accents ni casse. */
3402
+ function filtrerOptions(options, recherche) {
3403
+ const cherche = normaliserPourRecherche(recherche);
3404
+ if (cherche === '')
3405
+ return options;
3406
+ return options.filter((option) => normaliserPourRecherche(option.libelle).includes(cherche));
3407
+ }
3408
+
3409
+ /**
3410
+ * Liste deroulante, sur un `select` NATIF.
3411
+ *
3412
+ * ```html
3413
+ * <st-select libelle="Pays" [options]="pays" autocomplete="country" [formField]="adresse.pays" />
3414
+ * ```
3415
+ *
3416
+ * ⚠️ **Natif, et non une liste ARIA** (FE-4). Le `select` natif ouvre le selecteur
3417
+ * du systeme sur mobile, se remplit automatiquement, se parcourt a la lettre, et
3418
+ * se lit partout. Une liste recomposee en ARIA ne fait jamais mieux sur ces
3419
+ * points ; elle ne se justifie que pour filtrer une longue liste — c'est
3420
+ * `st-combobox` — ou choisir plusieurs valeurs — `st-multiselect`.
3421
+ *
3422
+ * La premiere option, vide, dit « Choisir… » : sans elle, la premiere vraie
3423
+ * option paraitrait choisie alors que personne ne l'a fait. Le modele vaut
3424
+ * alors `''`, que `required` refuse.
3425
+ *
3426
+ * `readonly` n'existe pas sur un `select` : le champ annonce `aria-readonly` et
3427
+ * ignore le changement, plutot que d'etre desactive — un champ desactive sort de
3428
+ * l'ordre de tabulation, et sa valeur ne se lit plus.
3429
+ */
3430
+ class StSelect extends ControleDeSaisie {
3431
+ libelles = inject(ST_LIBELLES);
3432
+ options = input.required(/* @ts-ignore */
3433
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
3434
+ /** Texte de l'option vide. Par defaut : « Choisir… ». */
3435
+ texteVide = input(null, /* @ts-ignore */
3436
+ ...(ngDevMode ? [{ debugName: "texteVide" }] : /* istanbul ignore next */ []));
3437
+ /** WCAG 1.3.5 : `country`, `sex`, `honorific-prefix`… */
3438
+ autocomplete = input(null, /* @ts-ignore */
3439
+ ...(ngDevMode ? [{ debugName: "autocomplete" }] : /* istanbul ignore next */ []));
3440
+ /** Options consecutives d'un meme groupe, dans l'ordre fourni. */
3441
+ blocs = computed(() => {
3442
+ const blocs = [];
3443
+ for (const option of this.options()) {
3444
+ const groupe = option.groupe ?? null;
3445
+ const dernier = blocs.at(-1);
3446
+ if (dernier?.groupe === groupe)
3447
+ dernier.options.push(option);
3448
+ else
3449
+ blocs.push({ groupe, options: [option] });
3450
+ }
3451
+ return blocs;
3452
+ }, /* @ts-ignore */
3453
+ ...(ngDevMode ? [{ debugName: "blocs" }] : /* istanbul ignore next */ []));
3454
+ surChoix(evenement) {
3455
+ const element = evenement.target;
3456
+ if (this.readonly()) {
3457
+ element.value = this.value();
3458
+ return;
3459
+ }
3460
+ this.value.set(element.value);
3461
+ }
3462
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StSelect, deps: null, target: i0.ɵɵFactoryTarget.Component });
3463
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StSelect, isStandalone: true, selector: "st-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, texteVide: { classPropertyName: "texteVide", publicName: "texteVide", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "st-select" }, usesInheritance: true, ngImport: i0, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <div class=\"st-select__cadre\">\n <select\n #champ\n class=\"st-select__champ\"\n stControleDeChamp\n [attr.name]=\"name() || null\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.aria-readonly]=\"readonly() ? 'true' : null\"\n [disabled]=\"disabled()\"\n (change)=\"surChoix($event)\"\n (blur)=\"touch.emit()\"\n >\n <option value=\"\" [selected]=\"value() === ''\">{{ texteVide() ?? libelles().choisir }}</option>\n @for (bloc of blocs(); track $index) {\n @if (bloc.groupe === null) {\n @for (option of bloc.options; track option.valeur) {\n <option\n [value]=\"option.valeur\"\n [selected]=\"option.valeur === value()\"\n [disabled]=\"option.desactivee === true\"\n >\n {{ option.libelle }}\n </option>\n }\n } @else {\n <optgroup [label]=\"bloc.groupe\">\n @for (option of bloc.options; track option.valeur) {\n <option\n [value]=\"option.valeur\"\n [selected]=\"option.valeur === value()\"\n [disabled]=\"option.desactivee === true\"\n >\n {{ option.libelle }}\n </option>\n }\n </optgroup>\n }\n }\n </select>\n <st-icon class=\"st-select__chevron\" nom=\"chevron-bas\" taille=\"sm\" />\n </div>\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-select__cadre{position:relative}.st-select__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-select__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-select__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-select__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-select__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-select__champ{padding-inline-end:calc(var(--st-espacement-3) * 2 + 1rem);cursor:pointer;appearance:none}.st-select__chevron{position:absolute;inset-block-start:50%;inset-inline-end:var(--st-espacement-3);color:var(--st-couleur-texte-discret);pointer-events:none;transform:translateY(-50%)}@media(forced-colors:active){.st-select__chevron{color:CanvasText}}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }, { kind: "component", type: StIcon, selector: "st-icon", inputs: ["nom", "taille", "ton", "etiquette"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3464
+ }
3465
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StSelect, decorators: [{
3466
+ type: Component,
3467
+ args: [{ selector: 'st-select', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp, StIcon], host: { class: 'st-select' }, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <div class=\"st-select__cadre\">\n <select\n #champ\n class=\"st-select__champ\"\n stControleDeChamp\n [attr.name]=\"name() || null\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.aria-readonly]=\"readonly() ? 'true' : null\"\n [disabled]=\"disabled()\"\n (change)=\"surChoix($event)\"\n (blur)=\"touch.emit()\"\n >\n <option value=\"\" [selected]=\"value() === ''\">{{ texteVide() ?? libelles().choisir }}</option>\n @for (bloc of blocs(); track $index) {\n @if (bloc.groupe === null) {\n @for (option of bloc.options; track option.valeur) {\n <option\n [value]=\"option.valeur\"\n [selected]=\"option.valeur === value()\"\n [disabled]=\"option.desactivee === true\"\n >\n {{ option.libelle }}\n </option>\n }\n } @else {\n <optgroup [label]=\"bloc.groupe\">\n @for (option of bloc.options; track option.valeur) {\n <option\n [value]=\"option.valeur\"\n [selected]=\"option.valeur === value()\"\n [disabled]=\"option.desactivee === true\"\n >\n {{ option.libelle }}\n </option>\n }\n </optgroup>\n }\n }\n </select>\n <st-icon class=\"st-select__chevron\" nom=\"chevron-bas\" taille=\"sm\" />\n </div>\n</st-form-field>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-select__cadre{position:relative}.st-select__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-select__champ{box-sizing:border-box;inline-size:100%;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-select__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-select__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-select__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-select__champ{padding-inline-end:calc(var(--st-espacement-3) * 2 + 1rem);cursor:pointer;appearance:none}.st-select__chevron{position:absolute;inset-block-start:50%;inset-inline-end:var(--st-espacement-3);color:var(--st-couleur-texte-discret);pointer-events:none;transform:translateY(-50%)}@media(forced-colors:active){.st-select__chevron{color:CanvasText}}\n"] }]
3468
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], texteVide: [{ type: i0.Input, args: [{ isSignal: true, alias: "texteVide", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }] } });
3469
+
3470
+ let compteur$2 = 0;
3471
+ /** Un lundi connu, pour nommer les jours de la semaine sans horloge. */
3472
+ const LUNDI_DE_REFERENCE = '2006-01-02';
3473
+ /** « Aujourd'hui » dans un fuseau : la date du poste n'est pas celle de la personne. */
3474
+ function dateDuJour(fuseau) {
3475
+ const parties = new Intl.DateTimeFormat('en-CA', {
3476
+ timeZone: fuseau,
3477
+ year: 'numeric',
3478
+ month: '2-digit',
3479
+ day: '2-digit',
3480
+ }).formatToParts(new Date());
3481
+ const partie = (type) => parties.find((p) => p.type === type)?.value ?? '';
3482
+ return `${partie('year')}-${partie('month')}-${partie('day')}`;
3483
+ }
3484
+ /**
3485
+ * Calendrier d'un mois, pour choisir une date.
3486
+ *
3487
+ * ```html
3488
+ * <st-calendar [selection]="depart()" dateMin="2026-09-17" (choix)="depart.set($event)" />
3489
+ * ```
3490
+ *
3491
+ * ⚠️ **Une aide, jamais la seule voie.** Dans `st-date-field`, le calendrier
3492
+ * complete un champ texte : une date de naissance se tape plus vite qu'elle ne se
3493
+ * cherche mois par mois, et un calendrier reste penible au lecteur d'ecran.
3494
+ *
3495
+ * ⚠️ **Le clavier navigue de DATE en date, pas de cellule en cellule** (decision
3496
+ * D7 de l'audit de la vague 3). Fleche droite le 30 septembre mene au 1er
3497
+ * octobre ; Page suivante change de mois, Maj + Page d'annee ; Debut et Fin vont
3498
+ * aux bords de la semaine de la LOCALE. La grille d'Angular Aria deplace un focus
3499
+ * entre cellules affichees : il aurait fallu la contourner sur chaque touche qui
3500
+ * compte, et elle aurait fait entrer le CDK dans les formulaires.
3501
+ *
3502
+ * Semaine commencant au jour de la locale, noms de jours et de mois dans la
3503
+ * langue courante, « aujourd'hui » dans le fuseau de la personne
3504
+ * (`FUSEAU_PAR_DEFAUT`) et non celui du poste.
3505
+ */
3506
+ class StCalendar {
3507
+ libelles = inject(ST_LIBELLES);
3508
+ locale = inject(ST_LOCALE);
3509
+ element = inject(ElementRef);
3510
+ injecteur = inject(Injector);
3511
+ aujourdHui = dateDuJour(inject(FUSEAU_PAR_DEFAUT));
3512
+ selection = input(null, /* @ts-ignore */
3513
+ ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
3514
+ dateMin = input(null, /* @ts-ignore */
3515
+ ...(ngDevMode ? [{ debugName: "dateMin" }] : /* istanbul ignore next */ []));
3516
+ dateMax = input(null, /* @ts-ignore */
3517
+ ...(ngDevMode ? [{ debugName: "dateMax" }] : /* istanbul ignore next */ []));
3518
+ /** Periode a faire ressortir, pour un choix de plage. */
3519
+ plage = input(null, /* @ts-ignore */
3520
+ ...(ngDevMode ? [{ debugName: "plage" }] : /* istanbul ignore next */ []));
3521
+ choix = output();
3522
+ /** Echap : le conteneur decide de fermer, et ou rendre le focus. */
3523
+ fermeture = output();
3524
+ identifiants = (() => {
3525
+ const base = `st-calendrier-${(compteur$2 += 1).toString()}`;
3526
+ return { titre: `${base}-titre`, aide: `${base}-aide` };
3527
+ })();
3528
+ /** La date qui porte le focus. Suit la selection, bornee. */
3529
+ active = linkedSignal(() => bornerDate(this.selection() ?? this.aujourdHui, this.dateMin(), this.dateMax()), /* @ts-ignore */
3530
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
3531
+ premierJour = computed(() => premierJourDeLaSemaine(this.locale()), /* @ts-ignore */
3532
+ ...(ngDevMode ? [{ debugName: "premierJour" }] : /* istanbul ignore next */ []));
3533
+ titre = computed(() => {
3534
+ const [annee, mois] = this.active().split('-').map(Number);
3535
+ return new Intl.DateTimeFormat(this.locale(), {
3536
+ month: 'long',
3537
+ year: 'numeric',
3538
+ timeZone: 'UTC',
3539
+ }).format(Date.UTC(annee ?? 2000, (mois ?? 1) - 1, 1));
3540
+ }, /* @ts-ignore */
3541
+ ...(ngDevMode ? [{ debugName: "titre" }] : /* istanbul ignore next */ []));
3542
+ joursDeLaSemaine = computed(() => {
3543
+ const locale = this.locale();
3544
+ const court = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
3545
+ const long = new Intl.DateTimeFormat(locale, { weekday: 'long', timeZone: 'UTC' });
3546
+ return Array.from({ length: 7 }, (_, index) => {
3547
+ const date = ajouterJours(LUNDI_DE_REFERENCE, (this.premierJour() - 1 + index) % 7);
3548
+ const instant = Date.parse(`${date}T00:00:00Z`);
3549
+ return { court: court.format(instant), long: long.format(instant) };
3550
+ });
3551
+ }, /* @ts-ignore */
3552
+ ...(ngDevMode ? [{ debugName: "joursDeLaSemaine" }] : /* istanbul ignore next */ []));
3553
+ semaines = computed(() => {
3554
+ const active = this.active();
3555
+ const [annee, mois] = active.split('-').map(Number);
3556
+ const locale = this.locale();
3557
+ const lecture = new Intl.DateTimeFormat(locale, { dateStyle: 'full', timeZone: 'UTC' });
3558
+ const numero = new Intl.NumberFormat(locale);
3559
+ const selection = this.selection();
3560
+ const debut = this.plage()?.debut ?? null;
3561
+ const fin = this.plage()?.fin ?? null;
3562
+ const min = this.dateMin();
3563
+ const max = this.dateMax();
3564
+ const prefixe = active.slice(0, 7);
3565
+ return semainesDuMois(annee ?? 2000, mois ?? 1, this.premierJour()).map((semaine) => semaine.map((date) => ({
3566
+ date,
3567
+ horsDuMois: !date.startsWith(prefixe),
3568
+ numero: numero.format(Number(date.slice(8))),
3569
+ nom: lecture.format(Date.parse(`${date}T00:00:00Z`)),
3570
+ horsBornes: (min !== null && date < min) || (max !== null && date > max),
3571
+ choisi: date === selection || date === debut || date === fin,
3572
+ dansLaPlage: debut !== null && fin !== null && date > debut && date < fin,
3573
+ aujourdHui: date === this.aujourdHui,
3574
+ })));
3575
+ }, /* @ts-ignore */
3576
+ ...(ngDevMode ? [{ debugName: "semaines" }] : /* istanbul ignore next */ []));
3577
+ moisPrecedentPossible = computed(() => {
3578
+ const min = this.dateMin();
3579
+ return min === null || this.active().slice(0, 7) > min.slice(0, 7);
3580
+ }, /* @ts-ignore */
3581
+ ...(ngDevMode ? [{ debugName: "moisPrecedentPossible" }] : /* istanbul ignore next */ []));
3582
+ moisSuivantPossible = computed(() => {
3583
+ const max = this.dateMax();
3584
+ return max === null || this.active().slice(0, 7) < max.slice(0, 7);
3585
+ }, /* @ts-ignore */
3586
+ ...(ngDevMode ? [{ debugName: "moisSuivantPossible" }] : /* istanbul ignore next */ []));
3587
+ estActive(date) {
3588
+ return date === this.active();
3589
+ }
3590
+ /** Amene le focus sur la date active. */
3591
+ focus() {
3592
+ this.element.nativeElement
3593
+ .querySelector('button[data-date][tabindex="0"]')
3594
+ ?.focus();
3595
+ }
3596
+ changerDeMois(ecart) {
3597
+ this.active.set(bornerDate(ajouterMois(this.active(), ecart), this.dateMin(), this.dateMax()));
3598
+ }
3599
+ choisir(jour) {
3600
+ if (jour.horsBornes)
3601
+ return;
3602
+ this.active.set(jour.date);
3603
+ this.choix.emit(jour.date);
3604
+ }
3605
+ surTouche(evenement) {
3606
+ const active = this.active();
3607
+ const droiteAGauche = getComputedStyle(this.element.nativeElement).direction === 'rtl';
3608
+ const recul = (jourDeLaSemaine(active) - this.premierJour() + 7) % 7;
3609
+ const mois = evenement.shiftKey ? 12 : 1;
3610
+ const cibles = {
3611
+ ArrowRight: () => ajouterJours(active, droiteAGauche ? -1 : 1),
3612
+ ArrowLeft: () => ajouterJours(active, droiteAGauche ? 1 : -1),
3613
+ ArrowDown: () => ajouterJours(active, 7),
3614
+ ArrowUp: () => ajouterJours(active, -7),
3615
+ Home: () => ajouterJours(active, -recul),
3616
+ End: () => ajouterJours(active, 6 - recul),
3617
+ PageDown: () => ajouterMois(active, mois),
3618
+ PageUp: () => ajouterMois(active, -mois),
3619
+ };
3620
+ if (evenement.key === 'Escape') {
3621
+ evenement.preventDefault();
3622
+ this.fermeture.emit();
3623
+ return;
3624
+ }
3625
+ // Les fleches ne deplacent la date que depuis la grille : sur les boutons de
3626
+ // mois, elles gardent leur sens pour le navigateur et le lecteur d'ecran.
3627
+ const depuisLaGrille = evenement.target.hasAttribute('data-date');
3628
+ const cible = cibles[evenement.key];
3629
+ if (cible === undefined || !depuisLaGrille)
3630
+ return;
3631
+ evenement.preventDefault();
3632
+ this.active.set(bornerDate(cible(), this.dateMin(), this.dateMax()));
3633
+ afterNextRender(() => {
3634
+ this.focus();
3635
+ }, { injector: this.injecteur });
3636
+ }
3637
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StCalendar, deps: [], target: i0.ɵɵFactoryTarget.Component });
3638
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StCalendar, isStandalone: true, selector: "st-calendar", inputs: { selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, dateMin: { classPropertyName: "dateMin", publicName: "dateMin", isSignal: true, isRequired: false, transformFunction: null }, dateMax: { classPropertyName: "dateMax", publicName: "dateMax", isSignal: true, isRequired: false, transformFunction: null }, plage: { classPropertyName: "plage", publicName: "plage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { choix: "choix", fermeture: "fermeture" }, host: { listeners: { "keydown": "surTouche($event)" }, classAttribute: "st-calendar" }, ngImport: i0, template: "<div class=\"st-calendar__entete\">\n <!-- eslint-disable-next-line @angular-eslint/template/elements-content --\n Le contenu vient du gabarit de stIconButton, et l'etiquette est requise. -->\n <button\n stIconButton\n type=\"button\"\n icone=\"chevron-gauche\"\n [etiquette]=\"libelles().moisPrecedent\"\n [disabled]=\"!moisPrecedentPossible()\"\n (click)=\"changerDeMois(-1)\"\n ></button>\n <!-- Region vivante : changer de mois a la souris ne deplace pas le focus, et le\n nouveau mois doit se dire. -->\n <p class=\"st-calendar__titre\" aria-live=\"polite\" [id]=\"identifiants.titre\">{{ titre() }}</p>\n <!-- eslint-disable-next-line @angular-eslint/template/elements-content --\n Le contenu vient du gabarit de stIconButton, et l'etiquette est requise. -->\n <button\n stIconButton\n type=\"button\"\n icone=\"chevron-droite\"\n [etiquette]=\"libelles().moisSuivant\"\n [disabled]=\"!moisSuivantPossible()\"\n (click)=\"changerDeMois(1)\"\n ></button>\n</div>\n\n<table\n class=\"st-calendar__grille\"\n role=\"grid\"\n [attr.aria-labelledby]=\"identifiants.titre\"\n [attr.aria-describedby]=\"identifiants.aide\"\n>\n <thead>\n <tr>\n @for (jour of joursDeLaSemaine(); track $index) {\n <th class=\"st-calendar__jour-de-semaine\" scope=\"col\" [attr.abbr]=\"jour.long\">\n {{ jour.court }}\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (semaine of semaines(); track semaine[0]?.date) {\n <tr>\n @for (jour of semaine; track jour.date) {\n @if (jour.horsDuMois) {\n <td class=\"st-calendar__vide\"></td>\n } @else {\n <td [attr.aria-selected]=\"jour.choisi ? 'true' : null\">\n <button\n class=\"st-calendar__jour\"\n type=\"button\"\n [attr.data-date]=\"jour.date\"\n [attr.data-plage]=\"jour.dansLaPlage ? '' : null\"\n [attr.tabindex]=\"estActive(jour.date) ? 0 : -1\"\n [attr.aria-label]=\"jour.nom\"\n [attr.aria-current]=\"jour.aujourdHui ? 'date' : null\"\n [attr.aria-disabled]=\"jour.horsBornes ? 'true' : null\"\n (click)=\"choisir(jour)\"\n >\n {{ jour.numero }}\n </button>\n </td>\n }\n }\n </tr>\n }\n </tbody>\n</table>\n\n<p class=\"st-calendar__aide\" [id]=\"identifiants.aide\">{{ libelles().aideDuCalendrier }}</p>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:inline-block;padding:var(--st-espacement-3);color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-md)}.st-calendar__entete{display:flex;gap:var(--st-espacement-2);align-items:center;justify-content:space-between;margin-block-end:var(--st-espacement-2)}.st-calendar__titre{margin:0;font-weight:var(--st-graisse-semi);text-transform:capitalize}.st-calendar__grille{border-collapse:collapse}.st-calendar__jour-de-semaine{padding-block-end:var(--st-espacement-1);font-size:var(--st-taille-sm);font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte-discret);text-align:center}.st-calendar__jour:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-calendar__jour{inline-size:var(--st-cible-tactile-min);block-size:var(--st-cible-tactile-min);font:inherit;font-variant-numeric:tabular-nums;color:inherit;cursor:pointer;background:transparent;border:1px solid transparent;border-radius:var(--st-rayon-sm)}.st-calendar__jour:hover:not([aria-disabled=true]){background:var(--st-couleur-surface-enfoncee)}.st-calendar__jour[aria-current=date]{font-weight:var(--st-graisse-semi);border-color:var(--st-couleur-bordure-forte)}.st-calendar__jour[data-plage]{background:var(--st-couleur-surface-enfoncee)}.st-calendar__jour[aria-disabled=true]{color:var(--st-couleur-texte-desactive);text-decoration:line-through;cursor:not-allowed}[aria-selected=true]>.st-calendar__jour{font-weight:var(--st-graisse-semi);color:var(--st-couleur-sur-primaire);background:var(--st-couleur-primaire)}.st-calendar__aide{max-inline-size:20rem;margin:var(--st-espacement-2) 0 0;font-size:var(--st-taille-sm);color:var(--st-couleur-texte-discret)}@media(forced-colors:active){[aria-selected=true]>.st-calendar__jour{forced-color-adjust:none;color:HighlightText;background:Highlight}}\n"], dependencies: [{ kind: "component", type: StIconButton, selector: "button[stIconButton]", inputs: ["icone", "etiquette", "variante", "taille", "enfonce"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3639
+ }
3640
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StCalendar, decorators: [{
3641
+ type: Component,
3642
+ args: [{ selector: 'st-calendar', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StIconButton], host: {
3643
+ class: 'st-calendar',
3644
+ '(keydown)': 'surTouche($event)',
3645
+ }, template: "<div class=\"st-calendar__entete\">\n <!-- eslint-disable-next-line @angular-eslint/template/elements-content --\n Le contenu vient du gabarit de stIconButton, et l'etiquette est requise. -->\n <button\n stIconButton\n type=\"button\"\n icone=\"chevron-gauche\"\n [etiquette]=\"libelles().moisPrecedent\"\n [disabled]=\"!moisPrecedentPossible()\"\n (click)=\"changerDeMois(-1)\"\n ></button>\n <!-- Region vivante : changer de mois a la souris ne deplace pas le focus, et le\n nouveau mois doit se dire. -->\n <p class=\"st-calendar__titre\" aria-live=\"polite\" [id]=\"identifiants.titre\">{{ titre() }}</p>\n <!-- eslint-disable-next-line @angular-eslint/template/elements-content --\n Le contenu vient du gabarit de stIconButton, et l'etiquette est requise. -->\n <button\n stIconButton\n type=\"button\"\n icone=\"chevron-droite\"\n [etiquette]=\"libelles().moisSuivant\"\n [disabled]=\"!moisSuivantPossible()\"\n (click)=\"changerDeMois(1)\"\n ></button>\n</div>\n\n<table\n class=\"st-calendar__grille\"\n role=\"grid\"\n [attr.aria-labelledby]=\"identifiants.titre\"\n [attr.aria-describedby]=\"identifiants.aide\"\n>\n <thead>\n <tr>\n @for (jour of joursDeLaSemaine(); track $index) {\n <th class=\"st-calendar__jour-de-semaine\" scope=\"col\" [attr.abbr]=\"jour.long\">\n {{ jour.court }}\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (semaine of semaines(); track semaine[0]?.date) {\n <tr>\n @for (jour of semaine; track jour.date) {\n @if (jour.horsDuMois) {\n <td class=\"st-calendar__vide\"></td>\n } @else {\n <td [attr.aria-selected]=\"jour.choisi ? 'true' : null\">\n <button\n class=\"st-calendar__jour\"\n type=\"button\"\n [attr.data-date]=\"jour.date\"\n [attr.data-plage]=\"jour.dansLaPlage ? '' : null\"\n [attr.tabindex]=\"estActive(jour.date) ? 0 : -1\"\n [attr.aria-label]=\"jour.nom\"\n [attr.aria-current]=\"jour.aujourdHui ? 'date' : null\"\n [attr.aria-disabled]=\"jour.horsBornes ? 'true' : null\"\n (click)=\"choisir(jour)\"\n >\n {{ jour.numero }}\n </button>\n </td>\n }\n }\n </tr>\n }\n </tbody>\n</table>\n\n<p class=\"st-calendar__aide\" [id]=\"identifiants.aide\">{{ libelles().aideDuCalendrier }}</p>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:inline-block;padding:var(--st-espacement-3);color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-md)}.st-calendar__entete{display:flex;gap:var(--st-espacement-2);align-items:center;justify-content:space-between;margin-block-end:var(--st-espacement-2)}.st-calendar__titre{margin:0;font-weight:var(--st-graisse-semi);text-transform:capitalize}.st-calendar__grille{border-collapse:collapse}.st-calendar__jour-de-semaine{padding-block-end:var(--st-espacement-1);font-size:var(--st-taille-sm);font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte-discret);text-align:center}.st-calendar__jour:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-calendar__jour{inline-size:var(--st-cible-tactile-min);block-size:var(--st-cible-tactile-min);font:inherit;font-variant-numeric:tabular-nums;color:inherit;cursor:pointer;background:transparent;border:1px solid transparent;border-radius:var(--st-rayon-sm)}.st-calendar__jour:hover:not([aria-disabled=true]){background:var(--st-couleur-surface-enfoncee)}.st-calendar__jour[aria-current=date]{font-weight:var(--st-graisse-semi);border-color:var(--st-couleur-bordure-forte)}.st-calendar__jour[data-plage]{background:var(--st-couleur-surface-enfoncee)}.st-calendar__jour[aria-disabled=true]{color:var(--st-couleur-texte-desactive);text-decoration:line-through;cursor:not-allowed}[aria-selected=true]>.st-calendar__jour{font-weight:var(--st-graisse-semi);color:var(--st-couleur-sur-primaire);background:var(--st-couleur-primaire)}.st-calendar__aide{max-inline-size:20rem;margin:var(--st-espacement-2) 0 0;font-size:var(--st-taille-sm);color:var(--st-couleur-texte-discret)}@media(forced-colors:active){[aria-selected=true]>.st-calendar__jour{forced-color-adjust:none;color:HighlightText;background:Highlight}}\n"] }]
3646
+ }], propDecorators: { selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], dateMin: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateMin", required: false }] }], dateMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateMax", required: false }] }], plage: [{ type: i0.Input, args: [{ isSignal: true, alias: "plage", required: false }] }], choix: [{ type: i0.Output, args: ["choix"] }], fermeture: [{ type: i0.Output, args: ["fermeture"] }] } });
3647
+
3648
+ /** Deux choix du meme fichier ne l'ajoutent pas deux fois. */
3649
+ function empreinte(fichier) {
3650
+ return `${fichier.name}|${fichier.size.toString()}|${fichier.lastModified.toString()}`;
3651
+ }
3652
+ /**
3653
+ * Choix de fichiers, sur un `input type="file"` natif.
3654
+ *
3655
+ * ```html
3656
+ * <st-file-upload libelle="Justificatifs" accept=".pdf, image/*" [tailleMax]="5_000_000" multiple
3657
+ * [formField]="dossier.pieces" />
3658
+ * <!-- schema : validate(chemin.pieces, ({ value }) => erreursDeFichiers(value(), regles)) -->
3659
+ * ```
3660
+ *
3661
+ * Le modele est la liste des `File` choisis. L'envoi appartient a l'application,
3662
+ * par `@starterlib/core` : le composant choisit, il ne televerse pas.
3663
+ *
3664
+ * ⚠️ **L'element natif reste la voie principale** (FE-4) : il ouvre le selecteur
3665
+ * du systeme, l'appareil photo sur mobile, et se commande au clavier. Il est
3666
+ * masque visuellement, pas retire : le bouton affiche est son etiquette. Le
3667
+ * depot par glisser est un complement a la souris, jamais la seule voie.
3668
+ *
3669
+ * ⚠️ **Aucun apercu d'image** (SEC-7). Un apercu passe par une adresse `blob:`
3670
+ * que la politique de securite du contenu doit autoriser, et un SVG affiche peut
3671
+ * porter du script. Le nom et la taille suffisent a reconnaitre un fichier.
3672
+ *
3673
+ * ⚠️ **`accept`, `tailleMax` et `nombreMax` sont un confort** (SEC-5) : ils
3674
+ * s'affichent en aide et filtrent le selecteur du systeme, mais la verification
3675
+ * est dans le schema — `erreursDeFichiers` —, et le serveur relit le contenu.
3676
+ *
3677
+ * Chaque changement de la liste s'annonce : « 2 fichiers choisis ». Un fichier
3678
+ * retire rend le focus au bouton de retrait suivant, ou au choix de fichiers : le
3679
+ * focus ne tombe jamais sur le document.
3680
+ */
3681
+ class StFileUpload extends ContratDeChamp {
3682
+ libelles = inject(ST_LIBELLES);
3683
+ locale = inject(ST_LOCALE);
3684
+ element = inject(ElementRef);
3685
+ injecteur = inject(Injector);
3686
+ value = model([], /* @ts-ignore */
3687
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
3688
+ /** Attribut `accept` natif : `.pdf, image/*`. */
3689
+ accept = input('', /* @ts-ignore */
3690
+ ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
3691
+ multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3692
+ /** En octets. Affiche en aide ; la verification est dans le schema. */
3693
+ tailleMax = input(null, /* @ts-ignore */
3694
+ ...(ngDevMode ? [{ debugName: "tailleMax" }] : /* istanbul ignore next */ []));
3695
+ annonce = new AnnonceLocale(inject(DestroyRef));
3696
+ survol = signal(false, /* @ts-ignore */
3697
+ ...(ngDevMode ? [{ debugName: "survol" }] : /* istanbul ignore next */ []));
3698
+ aideLue = computed(() => {
3699
+ const libelles = this.libelles();
3700
+ const textes = [this.aide()];
3701
+ if (this.accept().trim() !== '')
3702
+ textes.push(libelles.typesAcceptes(this.accept()));
3703
+ const taille = this.tailleMax();
3704
+ if (taille !== null)
3705
+ textes.push(libelles.tailleMaximale(formaterTaille(taille, this.locale())));
3706
+ const aide = textes.filter((texte) => texte !== null).join(' ');
3707
+ return aide === '' ? null : aide;
3708
+ }, /* @ts-ignore */
3709
+ ...(ngDevMode ? [{ debugName: "aideLue" }] : /* istanbul ignore next */ []));
3710
+ lignes = computed(() => {
3711
+ const locale = this.locale();
3712
+ return this.value().map((fichier) => ({
3713
+ fichier,
3714
+ cle: empreinte(fichier),
3715
+ taille: formaterTaille(fichier.size, locale),
3716
+ }));
3717
+ }, /* @ts-ignore */
3718
+ ...(ngDevMode ? [{ debugName: "lignes" }] : /* istanbul ignore next */ []));
3719
+ surChoix(evenement) {
3720
+ const element = evenement.target;
3721
+ this.ajouter(Array.from(element.files ?? []));
3722
+ // Vide, pour qu'un second choix du meme fichier declenche encore `change`.
3723
+ element.value = '';
3724
+ }
3725
+ surSurvol(evenement) {
3726
+ if (this.disabled() || this.readonly())
3727
+ return;
3728
+ evenement.preventDefault();
3729
+ this.survol.set(true);
3730
+ }
3731
+ surDepot(evenement) {
3732
+ evenement.preventDefault();
3733
+ this.survol.set(false);
3734
+ if (this.disabled() || this.readonly())
3735
+ return;
3736
+ this.ajouter(Array.from(evenement.dataTransfer?.files ?? []));
3737
+ }
3738
+ retirer(indice) {
3739
+ const restants = this.value().filter((_, position) => position !== indice);
3740
+ this.value.set(restants);
3741
+ this.annonce.annoncer(this.libelles().nombreDeFichiers(restants.length));
3742
+ afterNextRender(() => {
3743
+ const boutons = this.element.nativeElement.querySelectorAll('.st-file-upload__retrait');
3744
+ const suivant = boutons[Math.min(indice, boutons.length - 1)];
3745
+ if (suivant !== undefined)
3746
+ suivant.focus();
3747
+ else
3748
+ this.focus();
3749
+ }, { injector: this.injecteur });
3750
+ }
3751
+ aLaSortie(evenement) {
3752
+ const suivant = evenement.relatedTarget;
3753
+ if (suivant !== null && this.element.nativeElement.contains(suivant))
3754
+ return;
3755
+ this.touch.emit();
3756
+ }
3757
+ ajouter(fichiers) {
3758
+ if (fichiers.length === 0 || this.disabled() || this.readonly())
3759
+ return;
3760
+ let liste;
3761
+ if (this.multiple()) {
3762
+ const connus = new Set(this.value().map(empreinte));
3763
+ liste = [...this.value(), ...fichiers.filter((fichier) => !connus.has(empreinte(fichier)))];
3764
+ }
3765
+ else {
3766
+ liste = fichiers.slice(0, 1);
3767
+ }
3768
+ this.value.set(liste);
3769
+ // Le choix est complet : ses erreurs — taille, type — se montrent tout de suite.
3770
+ this.touch.emit();
3771
+ this.annonce.annoncer(this.libelles().nombreDeFichiers(liste.length));
3772
+ }
3773
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StFileUpload, deps: null, target: i0.ɵɵFactoryTarget.Component });
3774
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StFileUpload, isStandalone: true, selector: "st-file-upload", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, tailleMax: { classPropertyName: "tailleMax", publicName: "tailleMax", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { listeners: { "focusout": "aLaSortie($event)" }, classAttribute: "st-file-upload" }, usesInheritance: true, ngImport: i0, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <div\n class=\"st-file-upload__zone\"\n [attr.data-survol]=\"survol() ? '' : null\"\n [attr.data-desactive]=\"disabled() ? '' : null\"\n (dragover)=\"surSurvol($event)\"\n (dragleave)=\"survol.set(false)\"\n (drop)=\"surDepot($event)\"\n >\n <input\n #champ\n class=\"st-file-upload__natif\"\n stControleDeChamp\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled() || readonly()\"\n (change)=\"surChoix($event)\"\n />\n <!-- Voie de la souris vers le champ natif. Masque au lecteur d'ecran et hors\n tabulation : le clavier et le lecteur d'ecran passent par le champ natif,\n deja nomme par l'etiquette du champ. -->\n <span class=\"st-file-upload__bouton\" aria-hidden=\"true\" (click)=\"champ.click()\">\n <st-icon nom=\"televerser\" taille=\"sm\" />\n {{ multiple() ? libelles().choisirDesFichiers : libelles().choisirUnFichier }}\n </span>\n @if (multiple()) {\n <span class=\"st-file-upload__depot\" aria-hidden=\"true\">{{ libelles().ouDeposerIci }}</span>\n }\n </div>\n\n @if (lignes().length > 0) {\n <ul class=\"st-file-upload__liste\">\n @for (ligne of lignes(); track ligne.cle; let indice = $index) {\n <li class=\"st-file-upload__fichier\">\n <st-icon nom=\"fichier\" taille=\"sm\" />\n <span class=\"st-file-upload__nom\">{{ ligne.fichier.name }}</span>\n <span class=\"st-file-upload__taille\">{{ ligne.taille }}</span>\n <!-- eslint-disable-next-line @angular-eslint/template/elements-content --\n Le contenu vient du gabarit de stIconButton, et l'etiquette est requise. -->\n <button\n class=\"st-file-upload__retrait\"\n stIconButton\n type=\"button\"\n icone=\"corbeille\"\n taille=\"sm\"\n [etiquette]=\"libelles().retirerLeFichier(ligne.fichier.name)\"\n [disabled]=\"disabled() || readonly()\"\n (click)=\"retirer(indice)\"\n ></button>\n </li>\n }\n </ul>\n }\n</st-form-field>\n<span class=\"st-vh\" aria-live=\"polite\">{{ annonce.texte() }}</span>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-file-upload__zone{position:relative;display:flex;flex-wrap:wrap;gap:var(--st-espacement-2) var(--st-espacement-3);align-items:center;padding:var(--st-espacement-3);border:1px dashed var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-md)}.st-file-upload__zone[data-survol]{background:var(--st-couleur-surface-enfoncee);border-style:solid}.st-file-upload__natif{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.st-file-upload__bouton{display:inline-flex;gap:var(--st-espacement-2);align-items:center;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-4);font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte);cursor:pointer;background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-file-upload__natif:focus-visible+.st-file-upload__bouton{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}[aria-invalid=true]+.st-file-upload__bouton{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-file-upload__natif:disabled+.st-file-upload__bouton{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-file-upload__depot{color:var(--st-couleur-texte-discret)}.st-file-upload__liste{display:grid;gap:var(--st-espacement-1);padding:0;margin:var(--st-espacement-2) 0 0;list-style:none}.st-file-upload__fichier{display:flex;gap:var(--st-espacement-2);align-items:center}.st-file-upload__nom{min-inline-size:0;overflow-wrap:anywhere}.st-file-upload__taille{flex:none;color:var(--st-couleur-texte-discret);font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "component", type: StFormField, selector: "st-form-field", inputs: ["libelle", "aide", "complement", "erreurs", "afficherErreurs", "obligatoire", "disposition", "libelleMasque"] }, { kind: "directive", type: StControleDeChamp, selector: "[stControleDeChamp]" }, { kind: "component", type: StIcon, selector: "st-icon", inputs: ["nom", "taille", "ton", "etiquette"] }, { kind: "component", type: StIconButton, selector: "button[stIconButton]", inputs: ["icone", "etiquette", "variante", "taille", "enfonce"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3775
+ }
3776
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StFileUpload, decorators: [{
3777
+ type: Component,
3778
+ args: [{ selector: 'st-file-upload', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp, StIcon, StIconButton], host: {
3779
+ class: 'st-file-upload',
3780
+ '(focusout)': 'aLaSortie($event)',
3781
+ }, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"errors()\"\n [afficherErreurs]=\"touched()\"\n [obligatoire]=\"required()\"\n>\n <div\n class=\"st-file-upload__zone\"\n [attr.data-survol]=\"survol() ? '' : null\"\n [attr.data-desactive]=\"disabled() ? '' : null\"\n (dragover)=\"surSurvol($event)\"\n (dragleave)=\"survol.set(false)\"\n (drop)=\"surDepot($event)\"\n >\n <input\n #champ\n class=\"st-file-upload__natif\"\n stControleDeChamp\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled() || readonly()\"\n (change)=\"surChoix($event)\"\n />\n <!-- Voie de la souris vers le champ natif. Masque au lecteur d'ecran et hors\n tabulation : le clavier et le lecteur d'ecran passent par le champ natif,\n deja nomme par l'etiquette du champ. -->\n <span class=\"st-file-upload__bouton\" aria-hidden=\"true\" (click)=\"champ.click()\">\n <st-icon nom=\"televerser\" taille=\"sm\" />\n {{ multiple() ? libelles().choisirDesFichiers : libelles().choisirUnFichier }}\n </span>\n @if (multiple()) {\n <span class=\"st-file-upload__depot\" aria-hidden=\"true\">{{ libelles().ouDeposerIci }}</span>\n }\n </div>\n\n @if (lignes().length > 0) {\n <ul class=\"st-file-upload__liste\">\n @for (ligne of lignes(); track ligne.cle; let indice = $index) {\n <li class=\"st-file-upload__fichier\">\n <st-icon nom=\"fichier\" taille=\"sm\" />\n <span class=\"st-file-upload__nom\">{{ ligne.fichier.name }}</span>\n <span class=\"st-file-upload__taille\">{{ ligne.taille }}</span>\n <!-- eslint-disable-next-line @angular-eslint/template/elements-content --\n Le contenu vient du gabarit de stIconButton, et l'etiquette est requise. -->\n <button\n class=\"st-file-upload__retrait\"\n stIconButton\n type=\"button\"\n icone=\"corbeille\"\n taille=\"sm\"\n [etiquette]=\"libelles().retirerLeFichier(ligne.fichier.name)\"\n [disabled]=\"disabled() || readonly()\"\n (click)=\"retirer(indice)\"\n ></button>\n </li>\n }\n </ul>\n }\n</st-form-field>\n<span class=\"st-vh\" aria-live=\"polite\">{{ annonce.texte() }}</span>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-file-upload__zone{position:relative;display:flex;flex-wrap:wrap;gap:var(--st-espacement-2) var(--st-espacement-3);align-items:center;padding:var(--st-espacement-3);border:1px dashed var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-md)}.st-file-upload__zone[data-survol]{background:var(--st-couleur-surface-enfoncee);border-style:solid}.st-file-upload__natif{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.st-file-upload__bouton{display:inline-flex;gap:var(--st-espacement-2);align-items:center;min-block-size:var(--st-cible-tactile-min);padding:var(--st-espacement-2) var(--st-espacement-4);font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte);cursor:pointer;background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure-forte);border-radius:var(--st-rayon-sm)}.st-file-upload__natif:focus-visible+.st-file-upload__bouton{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}[aria-invalid=true]+.st-file-upload__bouton{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-file-upload__natif:disabled+.st-file-upload__bouton{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-file-upload__depot{color:var(--st-couleur-texte-discret)}.st-file-upload__liste{display:grid;gap:var(--st-espacement-1);padding:0;margin:var(--st-espacement-2) 0 0;list-style:none}.st-file-upload__fichier{display:flex;gap:var(--st-espacement-2);align-items:center}.st-file-upload__nom{min-inline-size:0;overflow-wrap:anywhere}.st-file-upload__taille{flex:none;color:var(--st-couleur-texte-discret);font-variant-numeric:tabular-nums}\n"] }]
3782
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], tailleMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "tailleMax", required: false }] }] } });
3783
+
2097
3784
  let compteur$1 = 0;
2098
3785
  /**
2099
3786
  * Choix du theme : systeme, clair, sombre.
@@ -2731,5 +4418,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
2731
4418
  * Generated bundle index. Do not edit.
2732
4419
  */
2733
4420
 
2734
- export { AnnonceLocale, CHEMIN_DU_SPRITE, FUSEAU_PAR_DEFAUT, GABARITS_D_ETAT, IDENTIFIANT_DU_CONTENU_PRINCIPAL, LIBELLES_EN, LIBELLES_FR, NOMS_DICONE, REGION_VIVANTE_PARENTE, ST_LIBELLES, ST_LOCALE, StAppShell, StAvatar, StBadge, StBanner, StBreadcrumb, StButton, StButtonGroup, StCard, StChargement, StCode, StCopyButton, StCount, StDate, StDivider, StEmpty, StErreur, StError, StHeader, StHeading, StHorsLigne, StIcon, StIconButton, StInlineMessage, StLangSwitch, StLink, StLogo, StMasked, StMoney, StOfflineBanner, StPageTitle, StProgress, StRelativeTime, StSection, StSkeleton, StSkipLink, StSpinner, StState, StStatusPill, StSucces, StTag, StText, StThemeToggle, StTimer, StVide, copierDansLePressePapiers, formaterDuree, fournirCheminDuSprite, pressePapiersDisponible, provideStarterUi };
4421
+ export { AnnonceLocale, CHEMIN_DU_SPRITE, ContratDeChamp, ControleDeSaisie, FUSEAU_PAR_DEFAUT, GABARITS_D_ETAT, IDENTIFIANT_DU_CONTENU_PRINCIPAL, LIBELLES_EN, LIBELLES_FR, NOMS_DICONE, REGION_VIVANTE_PARENTE, ST_LIBELLES, ST_LOCALE, StAppShell, StAvatar, StBadge, StBanner, StBreadcrumb, StButton, StButtonGroup, StCalendar, StCard, StChargement, StCheckbox, StCode, StControleDeChamp, StCopyButton, StCount, StDate, StDivider, StEmpty, StErreur, StError, StErrorSummary, StFieldset, StFileUpload, StFormField, StHeader, StHeading, StHorsLigne, StIbanField, StIcon, StIconButton, StInlineMessage, StInput, StLangSwitch, StLink, StLogo, StMasked, StMoney, StOfflineBanner, StOtpField, StPageTitle, StPasswordField, StPhoneField, StProgress, StRadioGroup, StRelativeTime, StSearchField, StSection, StSelect, StSkeleton, StSkipLink, StSpinner, StState, StStatusPill, StSucces, StSwitch, StTag, StText, StTextarea, StThemeToggle, StTimer, StVide, copierDansLePressePapiers, erreurDate, erreurIban, erreurPlageDeDates, erreurTelephone, erreursDeFichiers, exempleDeDate, filtrerOptions, formaterDuree, fournirCheminDuSprite, fusionnerErreurs, messageDErreur, motifLocal, normaliserPourRecherche, pressePapiersDisponible, provideStarterUi };
2735
4422
  //# sourceMappingURL=starterlib-ui.mjs.map