@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.
package/README.md CHANGED
@@ -14,22 +14,24 @@ des dependances de pair : c'est l'application qui les installe, et elles doivent
14
14
  etre **identiques dans l'hote et dans chaque remote**. Deux versions du socle
15
15
  dans le meme navigateur, ce sont deux jeux de jetons d'injection.
16
16
 
17
- ## Trois points d'entree
17
+ ## Quatre points d'entree
18
18
 
19
19
  Un composant sort du tronc quand il traine un pair lourd, et seulement alors ;
20
20
  le point d'entree secondaire ou il va se choisit ensuite par ce qu'il EST pour
21
21
  qui l'emploie.
22
22
 
23
- | Point d'entree | Contenu | Ce qu'il traine | Gzippe |
24
- | ----------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------- | ------- |
25
- | `@starterlib/ui` | le tronc : etats, texte, actions, structure, transverses | `@angular/router`, deja exige par `core` | 22,4 Ko |
26
- | `@starterlib/ui/surfaces` | ce qui se pose AU-DESSUS de la page : menu, dialogues, tiroirs, messages ephemeres | `@angular/cdk`, `@angular/aria/menu` | 7,1 Ko |
27
- | `@starterlib/ui/interactions` | ce qui fait PARTIE de la page et traine Aria ou une surface : onglets, navigation laterale | `@angular/aria`, qui exige lui-meme `@angular/cdk` | 2,0 Ko |
23
+ | Point d'entree | Contenu | Ce qu'il traine a l'execution | Gzippe |
24
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------- |
25
+ | `@starterlib/ui` | le tronc : etats, texte, actions, structure, transverses, saisie native, calendrier | `@angular/router`, deja exige par `core` | 40,6 Ko |
26
+ | `@starterlib/ui/surfaces` | ce qui se pose AU-DESSUS de la page : menu, dialogues, tiroirs, messages ephemeres | `@angular/cdk`, `@angular/aria/menu` | 9,7 Ko |
27
+ | `@starterlib/ui/interactions` | ce qui fait PARTIE de la page et traine Aria ou une surface : onglets, navigation laterale, combobox | `@angular/aria`, qui exige lui-meme `@angular/cdk` | 6,9 Ko |
28
+ | `@starterlib/ui/formulaires` | les champs qui LISENT leur saisie : montant, date, plage de dates | le code d'`@angular/forms` (`transformedValue`) | 4,5 Ko |
28
29
 
29
30
  ```ts
30
- import { StButton, StAppShell } from '@starterlib/ui';
31
+ import { StButton, StInput, StSelect } from '@starterlib/ui';
31
32
  import { StDialog, StToast } from '@starterlib/ui/surfaces';
32
- import { StTabs, StTabPanel, StTabContent } from '@starterlib/ui/interactions';
33
+ import { StTabs, StCombobox, StMultiselect } from '@starterlib/ui/interactions';
34
+ import { StAmountField, StDateField } from '@starterlib/ui/formulaires';
33
35
  ```
34
36
 
35
37
  ⚠️ Un paquet partage en federation **ne s'elague pas** : declare singleton, il
@@ -38,17 +40,21 @@ pour but le poids, mais le **train de singletons** : une application qui
38
40
  n'affiche ni menu, ni dialogue, ni onglets ne charge ni le CDK ni Aria, et n'a
39
41
  pas a se redeployer quand ils montent de version.
40
42
 
41
- `@angular/cdk` et `@angular/aria` sont des pairs **optionnels** : le tronc ne les
42
- importe pas. Ils deviennent obligatoires des le premier import d'un point
43
- d'entree secondaire, et leur absence se voit alors a la resolution, pas a
44
- l'execution. ⚠️ `@angular/aria` exige `@angular/cdk` a la version EXACTE : les
45
- deux montent ensemble.
43
+ `@angular/cdk`, `@angular/aria` et `@angular/forms` sont des pairs
44
+ **optionnels** : le tronc n'en importe que des TYPES. Ils deviennent obligatoires
45
+ des le premier import d'un point d'entree secondaire qui les emploie, et leur
46
+ absence se voit alors a la resolution, pas a l'execution. ⚠️ `@angular/aria`
47
+ exige `@angular/cdk` a la version EXACTE : les deux montent ensemble.
46
48
 
47
- ⚠️ **En federation, chaque point d'entree se partage SEPAREMENT.** Ce sont trois
48
- specificateurs de module distincts : partager `@starterlib/ui` ne couvre ni
49
- `surfaces` ni `interactions`. Omis de la configuration, chaque remote en embarque
50
- sa propre copie — et la regle `singleton: true, strictVersion: true` qui protege
51
- le tronc ne protege plus rien au-dessus.
49
+ Ce que chaque point d'entree importe a l'execution est verifie sur le paquet
50
+ construit (`pnpm paquets:check`) : un import de trop dans le tronc echoue avant
51
+ la publication.
52
+
53
+ ⚠️ **En federation, chaque point d'entree se partage SEPAREMENT.** Ce sont quatre
54
+ specificateurs de module distincts : partager `@starterlib/ui` ne couvre aucun
55
+ des trois autres. Omis de la configuration, chaque remote en embarque sa propre
56
+ copie — et la regle `singleton: true, strictVersion: true` qui protege le tronc
57
+ ne protege plus rien au-dessus.
52
58
 
53
59
  ```js
54
60
  // federation.config.mjs de l'hote ET de chaque remote
@@ -56,6 +62,7 @@ shared: {
56
62
  '@starterlib/ui': { singleton: true, strictVersion: true },
57
63
  '@starterlib/ui/surfaces': { singleton: true, strictVersion: true },
58
64
  '@starterlib/ui/interactions': { singleton: true, strictVersion: true },
65
+ '@starterlib/ui/formulaires': { singleton: true, strictVersion: true },
59
66
  // ...
60
67
  }
61
68
  ```
@@ -148,8 +155,32 @@ Vague 2 :
148
155
  - **`ui/interactions`** — `st-tabs`, `st-tab-panel`, `stTabContent`, `st-sidenav`,
149
156
  `stSidenavToggle`.
150
157
 
151
- Inventaire et regles de la suite : `docs/plan-librairie.md` et
152
- `docs/audit-vague-2-2026-09-17.md` du depot.
158
+ Vague 3 — la saisie, sur Signal Forms :
159
+
160
+ - **Tronc** — `st-form-field`, `st-input`, `st-textarea`, `st-fieldset`,
161
+ `st-checkbox`, `st-radio-group`, `st-switch`, `st-error-summary`,
162
+ `st-password-field`, `st-otp-field`, `st-search-field`, `st-iban-field`,
163
+ `st-phone-field`, `st-select` (natif), `st-file-upload`, `st-calendar` ;
164
+ messages d'erreur traduits et fonctions de schema (`erreurIban`,
165
+ `erreurTelephone`, `erreurDate`, `erreurPlageDeDates`, `erreursDeFichiers`).
166
+ - **`ui/interactions`** — `st-combobox`, `st-multiselect`.
167
+ - **`ui/formulaires`** — `st-amount-field`, `st-date-field`, `st-date-range`.
168
+
169
+ Un champ s'emploie avec un formulaire, par `[formField]`, ou seul, par
170
+ `[(value)]`. Les regles d'un schema sont des fonctions PURES du socle, a brancher
171
+ par `validate` : les fournir toutes faites importerait `@angular/forms` a
172
+ l'execution dans le tronc.
173
+
174
+ ```ts
175
+ readonly virement = form(this.modele, (chemin) => {
176
+ required(chemin.beneficiaire);
177
+ validate(chemin.iban, ({ value }) => erreurIban(value()));
178
+ validate(chemin.execution, ({ value }) => erreurDate(value(), { min: aujourdHui }));
179
+ });
180
+ ```
181
+
182
+ Inventaire et regles de la suite : `docs/plan-librairie.md`,
183
+ `docs/audit-vague-2-2026-09-17.md` et `docs/audit-vague-3-2026-09-17.md` du depot.
153
184
 
154
185
  ## Regles tenues par les composants
155
186
 
@@ -6,6 +6,7 @@
6
6
  color, jamais par un attribut de gabarit. -->
7
7
  <svg xmlns="http://www.w3.org/2000/svg" style="display:none">
8
8
  <symbol id="boite-vide" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol>
9
+ <symbol id="calendrier" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></symbol>
9
10
  <symbol id="cercle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/></symbol>
10
11
  <symbol id="cercle-coche" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></symbol>
11
12
  <symbol id="cercle-croix" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></symbol>
@@ -18,6 +19,7 @@
18
19
  <symbol id="copie" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></symbol>
19
20
  <symbol id="corbeille" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></symbol>
20
21
  <symbol id="croix" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></symbol>
22
+ <symbol id="fichier" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></symbol>
21
23
  <symbol id="hors-ligne" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="1" y1="1" x2="23" y2="23"/><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/><path d="M10.71 5.05A16 16 0 0 1 22.58 9"/><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></symbol>
22
24
  <symbol id="lien-externe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></symbol>
23
25
  <symbol id="menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></symbol>
@@ -27,6 +29,7 @@
27
29
  <symbol id="rafraichir" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></symbol>
28
30
  <symbol id="recherche" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
29
31
  <symbol id="serveur" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></symbol>
32
+ <symbol id="televerser" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></symbol>
30
33
  <symbol id="triangle-alerte" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></symbol>
31
34
  <symbol id="utilisateur" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
32
35
  </svg>
@@ -0,0 +1,446 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, model, input, signal, computed, ChangeDetectionStrategy, Component, ElementRef, Injector, viewChild, afterNextRender } from '@angular/core';
3
+ import { transformedValue } from '@angular/forms/signals';
4
+ import { ContratDeChamp, ST_LIBELLES, ST_LOCALE, fusionnerErreurs, StFormField, StControleDeChamp, StCalendar, motifLocal, exempleDeDate, StIconButton, messageDErreur, StFieldset } from '@starterlib/ui';
5
+ import { lireMontant, decimalesDe, formaterMontant, montant, lireDate, formaterDateSaisie } from '@starterlib/util';
6
+
7
+ /**
8
+ * Montant, en UNITES MINEURES : `123456n` pour 1 234,56 EUR.
9
+ *
10
+ * ```html
11
+ * <st-amount-field libelle="Montant du virement" devise="EUR" [formField]="virement.montant" />
12
+ * ```
13
+ *
14
+ * ⚠️ **Jamais un nombre a virgule flottante** (SEC-8) : le modele est un `bigint`,
15
+ * lu chiffre par chiffre par `lireMontant`. `0,1 + 0,2` ne fait pas `0,3`, et
16
+ * 1 234,56 EUR ne doit pas devenir 1 234,5599999.
17
+ *
18
+ * ⚠️ **Un texte ambigu est refuse, jamais devine.** « 1.250 » en francais serait
19
+ * mille deux cent cinquante ou un virgule vingt-cinq : le champ dit l'erreur et
20
+ * garde le texte, plutot que d'envoyer un montant mille fois trop grand. Une
21
+ * saisie illisible laisse le MODELE inchange et rend le formulaire invalide, par
22
+ * `transformedValue` : il ne part pas avec l'ancien montant.
23
+ *
24
+ * Le groupement « 1 234,56 » s'applique a la sortie du champ, jamais pendant la
25
+ * frappe. `type="text"` et `inputmode="decimal"` : `type="number"` accepte
26
+ * « 1e5 », change la valeur a la molette, et ignore la virgule selon le navigateur.
27
+ *
28
+ * Les bornes — montant minimal, plafond — sont des regles du schema, en unites
29
+ * mineures ; le serveur les revalide (SEC-5).
30
+ */
31
+ class StAmountField extends ContratDeChamp {
32
+ libelles = inject(ST_LIBELLES);
33
+ locale = inject(ST_LOCALE);
34
+ value = model(null, /* @ts-ignore */
35
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
36
+ /** Code ISO 4217. Fixe l'echelle : 2 decimales en euros, 0 en yens. */
37
+ devise = input.required(/* @ts-ignore */
38
+ ...(ngDevMode ? [{ debugName: "devise" }] : /* istanbul ignore next */ []));
39
+ /** `transaction-amount` pour un paiement. */
40
+ autocomplete = input(null, /* @ts-ignore */
41
+ ...(ngDevMode ? [{ debugName: "autocomplete" }] : /* istanbul ignore next */ []));
42
+ /**
43
+ * Quitte au moins une fois. Relie a un formulaire, `touched` le dit ; employe
44
+ * seul, rien ne le dirait, et une saisie illisible ne s'afficherait jamais.
45
+ */
46
+ quitte = signal(false, /* @ts-ignore */
47
+ ...(ngDevMode ? [{ debugName: "quitte" }] : /* istanbul ignore next */ []));
48
+ saisie = transformedValue(this.value, {
49
+ parse: (texte) => {
50
+ if (texte.trim() === '')
51
+ return { value: null };
52
+ const devise = this.devise();
53
+ const lecture = lireMontant(texte, devise, this.locale());
54
+ if (lecture.lu)
55
+ return { value: lecture.unitesMineures };
56
+ return {
57
+ error: lecture.raison === 'decimales'
58
+ ? { kind: 'decimales', decimales: decimalesDe(devise) }
59
+ : { kind: 'montant', devise },
60
+ };
61
+ },
62
+ format: (valeur) => this.formater(valeur),
63
+ });
64
+ erreursAffichees = computed(() => fusionnerErreurs(this.errors(), this.saisie.parseErrors()), /* @ts-ignore */
65
+ ...(ngDevMode ? [{ debugName: "erreursAffichees" }] : /* istanbul ignore next */ []));
66
+ /** Symbole affiche a cote du champ ; le nom de la devise, lui, est lu. */
67
+ symbole = computed(() => new Intl.NumberFormat(this.locale(), { style: 'currency', currency: this.devise() })
68
+ .formatToParts(0)
69
+ .find((partie) => partie.type === 'currency')?.value ?? this.devise(), /* @ts-ignore */
70
+ ...(ngDevMode ? [{ debugName: "symbole" }] : /* istanbul ignore next */ []));
71
+ deviseLue = computed(() => {
72
+ const nom = new Intl.DisplayNames(this.locale(), { type: 'currency' }).of(this.devise()) ?? this.devise();
73
+ return this.libelles().deviseDuMontant(nom);
74
+ }, /* @ts-ignore */
75
+ ...(ngDevMode ? [{ debugName: "deviseLue" }] : /* istanbul ignore next */ []));
76
+ surSaisie(evenement) {
77
+ this.saisie.set(this.texteSaisi(evenement));
78
+ }
79
+ aLaSortie() {
80
+ // Regroupe « 1234,5 » en « 1 234,50 » ; un texte illisible reste tel quel,
81
+ // pour etre corrige et non retape.
82
+ if (this.saisie.parseErrors().length === 0)
83
+ this.saisie.set(this.formater(this.value()));
84
+ this.quitte.set(true);
85
+ this.touch.emit();
86
+ }
87
+ formater(valeur) {
88
+ if (valeur === null)
89
+ return '';
90
+ return formaterMontant(montant(valeur, this.devise()), {
91
+ locale: this.locale(),
92
+ sansDevise: true,
93
+ });
94
+ }
95
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StAmountField, deps: null, target: i0.ɵɵFactoryTarget.Component });
96
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StAmountField, isStandalone: true, selector: "st-amount-field", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, devise: { classPropertyName: "devise", publicName: "devise", isSignal: true, isRequired: true, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { classAttribute: "st-amount-field" }, usesInheritance: true, ngImport: i0, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [complement]=\"deviseLue()\"\n [erreurs]=\"erreursAffichees()\"\n [afficherErreurs]=\"touched() || quitte()\"\n [obligatoire]=\"required()\"\n>\n <div class=\"st-amount-field__ligne\">\n <input\n #champ\n class=\"st-amount-field__champ\"\n stControleDeChamp\n type=\"text\"\n inputmode=\"decimal\"\n spellcheck=\"false\"\n [attr.autocomplete]=\"autocomplete()\"\n [value]=\"saisie()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"aLaSortie()\"\n />\n <span class=\"st-amount-field__devise\" aria-hidden=\"true\">{{ symbole() }}</span>\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-amount-field__ligne{display:flex;gap:var(--st-espacement-2);align-items:center}.st-amount-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-amount-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-amount-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-amount-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-amount-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-amount-field__champ{max-inline-size:16rem;font-variant-numeric:tabular-nums;text-align:end}.st-amount-field__devise{color:var(--st-couleur-texte-discret)}\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 });
97
+ }
98
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StAmountField, decorators: [{
99
+ type: Component,
100
+ args: [{ selector: 'st-amount-field', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp], host: { class: 'st-amount-field' }, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aide()\"\n [complement]=\"deviseLue()\"\n [erreurs]=\"erreursAffichees()\"\n [afficherErreurs]=\"touched() || quitte()\"\n [obligatoire]=\"required()\"\n>\n <div class=\"st-amount-field__ligne\">\n <input\n #champ\n class=\"st-amount-field__champ\"\n stControleDeChamp\n type=\"text\"\n inputmode=\"decimal\"\n spellcheck=\"false\"\n [attr.autocomplete]=\"autocomplete()\"\n [value]=\"saisie()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"aLaSortie()\"\n />\n <span class=\"st-amount-field__devise\" aria-hidden=\"true\">{{ symbole() }}</span>\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-amount-field__ligne{display:flex;gap:var(--st-espacement-2);align-items:center}.st-amount-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-amount-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-amount-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-amount-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-amount-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-amount-field__champ{max-inline-size:16rem;font-variant-numeric:tabular-nums;text-align:end}.st-amount-field__devise{color:var(--st-couleur-texte-discret)}\n"] }]
101
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], devise: [{ type: i0.Input, args: [{ isSignal: true, alias: "devise", required: true }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }] } });
102
+
103
+ let compteur$1 = 0;
104
+ /**
105
+ * Date calendaire : un champ texte, et un calendrier pour qui prefere choisir.
106
+ *
107
+ * ```html
108
+ * <st-date-field libelle="Date de naissance" autocomplete="bday" [dateMax]="aujourdHui"
109
+ * [formField]="profil.naissance" />
110
+ * <!-- schema : validate(chemin.naissance, ({ value }) => erreurDate(value(), { max: aujourdHui })) -->
111
+ * ```
112
+ *
113
+ * Le modele est une date ISO — `2026-09-17` — ou `null`, jamais un objet `Date` :
114
+ * une date calendaire n'a ni heure ni fuseau.
115
+ *
116
+ * ⚠️ **Un champ texte d'abord** (decision D7 de l'audit de la vague 3). Le champ
117
+ * natif `type="date"` affiche son format dans la langue du NAVIGATEUR : il ne
118
+ * bascule pas quand l'application change de langue. Ici, le format attendu —
119
+ * `jj/mm/aaaa` ou `mm/dd/yyyy` — suit la langue courante, et il est dit en aide.
120
+ *
121
+ * ⚠️ **Le calendrier est une aide, pas la seule voie** : une date de naissance
122
+ * se tape plus vite qu'elle ne se cherche mois par mois. Il s'ouvre sous le champ,
123
+ * dans le flux de la page : pas de surcouche a positionner, rien de rogne dans
124
+ * un dialogue, et le CDK n'entre pas dans les formulaires.
125
+ *
126
+ * Une date illisible ou inexistante — « 31/02/2026 » — garde le texte, laisse le
127
+ * modele inchange et rend le formulaire invalide.
128
+ */
129
+ class StDateField extends ContratDeChamp {
130
+ libelles = inject(ST_LIBELLES);
131
+ locale = inject(ST_LOCALE);
132
+ element = inject(ElementRef);
133
+ injecteur = inject(Injector);
134
+ value = model(null, /* @ts-ignore */
135
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
136
+ /** Bornes du calendrier. Les memes que celles du schema : voir `erreurDate`. */
137
+ dateMin = input(null, /* @ts-ignore */
138
+ ...(ngDevMode ? [{ debugName: "dateMin" }] : /* istanbul ignore next */ []));
139
+ dateMax = input(null, /* @ts-ignore */
140
+ ...(ngDevMode ? [{ debugName: "dateMax" }] : /* istanbul ignore next */ []));
141
+ /** `bday` pour une date de naissance. */
142
+ autocomplete = input(null, /* @ts-ignore */
143
+ ...(ngDevMode ? [{ debugName: "autocomplete" }] : /* istanbul ignore next */ []));
144
+ ouvert = signal(false, /* @ts-ignore */
145
+ ...(ngDevMode ? [{ debugName: "ouvert" }] : /* istanbul ignore next */ []));
146
+ identifiantCalendrier = `st-date-${(compteur$1 += 1).toString()}-calendrier`;
147
+ calendrier = viewChild(StCalendar, /* @ts-ignore */
148
+ ...(ngDevMode ? [{ debugName: "calendrier" }] : /* istanbul ignore next */ []));
149
+ // `read` : sur un bouton qui porte le composant `stIconButton`, la reference
150
+ // rendrait le composant, et non l'element a focaliser.
151
+ bouton = viewChild('bouton', { ...(ngDevMode ? { debugName: "bouton" } : /* istanbul ignore next */ {}), read: ElementRef });
152
+ /**
153
+ * Quitte au moins une fois. Relie a un formulaire, `touched` le dit ; employe
154
+ * seul, rien ne le dirait, et une saisie illisible ne s'afficherait jamais.
155
+ */
156
+ quitte = signal(false, /* @ts-ignore */
157
+ ...(ngDevMode ? [{ debugName: "quitte" }] : /* istanbul ignore next */ []));
158
+ saisie = transformedValue(this.value, {
159
+ parse: (texte) => {
160
+ if (texte.trim() === '')
161
+ return { value: null };
162
+ const date = lireDate(texte, this.locale());
163
+ return date === null ? { error: { kind: 'date' } } : { value: date };
164
+ },
165
+ format: (valeur) => this.formater(valeur),
166
+ });
167
+ aideLue = computed(() => {
168
+ const libelles = this.libelles();
169
+ const locale = this.locale();
170
+ const format = libelles.formatDeDate(motifLocal(libelles, locale), exempleDeDate(locale));
171
+ const aide = this.aide();
172
+ return aide === null ? format : `${aide} ${format}`;
173
+ }, /* @ts-ignore */
174
+ ...(ngDevMode ? [{ debugName: "aideLue" }] : /* istanbul ignore next */ []));
175
+ erreursAffichees = computed(() => fusionnerErreurs(this.errors(), this.saisie.parseErrors()), /* @ts-ignore */
176
+ ...(ngDevMode ? [{ debugName: "erreursAffichees" }] : /* istanbul ignore next */ []));
177
+ surSaisie(evenement) {
178
+ this.saisie.set(this.texteSaisi(evenement));
179
+ }
180
+ aLaSortieDuChamp() {
181
+ // « 7/9/2026 » devient « 07/09/2026 » ; un texte illisible reste a corriger.
182
+ if (this.saisie.parseErrors().length === 0)
183
+ this.saisie.set(this.formater(this.value()));
184
+ }
185
+ basculer() {
186
+ if (this.ouvert()) {
187
+ this.fermer(false);
188
+ return;
189
+ }
190
+ this.ouvert.set(true);
191
+ afterNextRender(() => {
192
+ this.calendrier()?.focus();
193
+ }, { injector: this.injecteur });
194
+ }
195
+ choisir(date) {
196
+ this.value.set(date);
197
+ this.fermer(false);
198
+ // Retour au champ : la date choisie s'y relit, et se corrige au clavier.
199
+ afterNextRender(() => {
200
+ this.focus();
201
+ }, { injector: this.injecteur });
202
+ }
203
+ /** Echap rend le focus au bouton qui a ouvert le calendrier. */
204
+ fermer(rendreLeFocus) {
205
+ this.ouvert.set(false);
206
+ if (!rendreLeFocus)
207
+ return;
208
+ afterNextRender(() => {
209
+ this.bouton()?.nativeElement.focus();
210
+ }, { injector: this.injecteur });
211
+ }
212
+ aLaSortieDuComposant(evenement) {
213
+ const suivant = evenement.relatedTarget;
214
+ if (suivant !== null && this.element.nativeElement.contains(suivant))
215
+ return;
216
+ this.ouvert.set(false);
217
+ this.quitte.set(true);
218
+ this.touch.emit();
219
+ }
220
+ formater(valeur) {
221
+ return valeur === null ? '' : formaterDateSaisie(valeur, this.locale());
222
+ }
223
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDateField, deps: null, target: i0.ɵɵFactoryTarget.Component });
224
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StDateField, isStandalone: true, selector: "st-date-field", inputs: { value: { classPropertyName: "value", publicName: "value", 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 }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { listeners: { "focusout": "aLaSortieDuComposant($event)" }, classAttribute: "st-date-field" }, viewQueries: [{ propertyName: "calendrier", first: true, predicate: StCalendar, descendants: true, isSignal: true }, { propertyName: "bouton", first: true, predicate: ["bouton"], descendants: true, read: ElementRef, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"erreursAffichees()\"\n [afficherErreurs]=\"touched() || quitte()\"\n [obligatoire]=\"required()\"\n>\n <div class=\"st-date-field__ligne\">\n <input\n #champ\n class=\"st-date-field__champ\"\n stControleDeChamp\n type=\"text\"\n spellcheck=\"false\"\n [attr.autocomplete]=\"autocomplete()\"\n [value]=\"saisie()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"aLaSortieDuChamp()\"\n />\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 #bouton\n stIconButton\n type=\"button\"\n icone=\"calendrier\"\n [etiquette]=\"libelles().choisirDansLeCalendrier(libelle())\"\n [attr.aria-expanded]=\"ouvert()\"\n [attr.aria-controls]=\"ouvert() ? identifiantCalendrier : null\"\n [disabled]=\"disabled() || readonly()\"\n (click)=\"basculer()\"\n ></button>\n </div>\n @if (ouvert()) {\n <st-calendar\n class=\"st-date-field__calendrier\"\n [id]=\"identifiantCalendrier\"\n [selection]=\"value()\"\n [dateMin]=\"dateMin()\"\n [dateMax]=\"dateMax()\"\n (choix)=\"choisir($event)\"\n (fermeture)=\"fermer(true)\"\n />\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-date-field__ligne{display:flex;gap:var(--st-espacement-1);align-items:center}.st-date-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-date-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-date-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-date-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-date-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-date-field__champ{max-inline-size:12rem;font-variant-numeric:tabular-nums}.st-date-field__calendrier{margin-block-start:var(--st-espacement-2)}\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"] }, { kind: "component", type: StCalendar, selector: "st-calendar", inputs: ["selection", "dateMin", "dateMax", "plage"], outputs: ["choix", "fermeture"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
225
+ }
226
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDateField, decorators: [{
227
+ type: Component,
228
+ args: [{ selector: 'st-date-field', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFormField, StControleDeChamp, StIconButton, StCalendar], host: {
229
+ class: 'st-date-field',
230
+ '(focusout)': 'aLaSortieDuComposant($event)',
231
+ }, template: "<st-form-field\n [libelle]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"erreursAffichees()\"\n [afficherErreurs]=\"touched() || quitte()\"\n [obligatoire]=\"required()\"\n>\n <div class=\"st-date-field__ligne\">\n <input\n #champ\n class=\"st-date-field__champ\"\n stControleDeChamp\n type=\"text\"\n spellcheck=\"false\"\n [attr.autocomplete]=\"autocomplete()\"\n [value]=\"saisie()\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie($event)\"\n (blur)=\"aLaSortieDuChamp()\"\n />\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 #bouton\n stIconButton\n type=\"button\"\n icone=\"calendrier\"\n [etiquette]=\"libelles().choisirDansLeCalendrier(libelle())\"\n [attr.aria-expanded]=\"ouvert()\"\n [attr.aria-controls]=\"ouvert() ? identifiantCalendrier : null\"\n [disabled]=\"disabled() || readonly()\"\n (click)=\"basculer()\"\n ></button>\n </div>\n @if (ouvert()) {\n <st-calendar\n class=\"st-date-field__calendrier\"\n [id]=\"identifiantCalendrier\"\n [selection]=\"value()\"\n [dateMin]=\"dateMin()\"\n [dateMax]=\"dateMax()\"\n (choix)=\"choisir($event)\"\n (fermeture)=\"fermer(true)\"\n />\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-date-field__ligne{display:flex;gap:var(--st-espacement-1);align-items:center}.st-date-field__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-date-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-date-field__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-date-field__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-date-field__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-date-field__champ{max-inline-size:12rem;font-variant-numeric:tabular-nums}.st-date-field__calendrier{margin-block-start:var(--st-espacement-2)}\n"] }]
232
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], dateMin: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateMin", required: false }] }], dateMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateMax", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], calendrier: [{ type: i0.ViewChild, args: [i0.forwardRef(() => StCalendar), { isSignal: true }] }], bouton: [{ type: i0.ViewChild, args: ['bouton', { ...{
233
+ read: ElementRef,
234
+ }, isSignal: true }] }] } });
235
+
236
+ let compteur = 0;
237
+ /**
238
+ * Plage de dates : un debut, une fin, dans un groupe nomme par sa question.
239
+ *
240
+ * ```html
241
+ * <st-date-range libelle="Periode du sejour" [formField]="reservation.sejour" />
242
+ * <!-- schema : validate(chemin.sejour, ({ value }) => erreurPlageDeDates(value())) -->
243
+ * ```
244
+ *
245
+ * Deux champs texte etiquetes — « Date de debut », « Date de fin » — dans un
246
+ * `fieldset` dont la legende est le `libelle` : chacun se lit avec la question.
247
+ * Chaque champ a son calendrier ; celui de la fin s'ouvre au mois du debut et
248
+ * refuse les jours qui le precedent.
249
+ *
250
+ * ⚠️ **Une erreur dit QUELLE date corriger** : « Date de fin : saisissez une date
251
+ * qui existe… ». Sans le nom de la partie, deux messages identiques ne disent pas
252
+ * lequel des champs est faux. L'erreur de lecture marque aussi son champ
253
+ * `aria-invalid`, et lui seul.
254
+ *
255
+ * L'obligation d'une borne est une regle du schema : `required` ne sait pas ce
256
+ * qu'est une plage « vide ».
257
+ */
258
+ class StDateRange extends ContratDeChamp {
259
+ libelles = inject(ST_LIBELLES);
260
+ locale = inject(ST_LOCALE);
261
+ element = inject(ElementRef);
262
+ injecteur = inject(Injector);
263
+ value = model({ debut: null, fin: null }, /* @ts-ignore */
264
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
265
+ dateMin = input(null, /* @ts-ignore */
266
+ ...(ngDevMode ? [{ debugName: "dateMin" }] : /* istanbul ignore next */ []));
267
+ dateMax = input(null, /* @ts-ignore */
268
+ ...(ngDevMode ? [{ debugName: "dateMax" }] : /* istanbul ignore next */ []));
269
+ /** Etiquettes des deux champs. Par defaut : « Date de debut », « Date de fin ». */
270
+ libelleDebut = input(null, /* @ts-ignore */
271
+ ...(ngDevMode ? [{ debugName: "libelleDebut" }] : /* istanbul ignore next */ []));
272
+ libelleFin = input(null, /* @ts-ignore */
273
+ ...(ngDevMode ? [{ debugName: "libelleFin" }] : /* istanbul ignore next */ []));
274
+ parties = ['debut', 'fin'];
275
+ ouvert = signal(null, /* @ts-ignore */
276
+ ...(ngDevMode ? [{ debugName: "ouvert" }] : /* istanbul ignore next */ []));
277
+ identifiants = (() => {
278
+ const base = `st-plage-${(compteur += 1).toString()}`;
279
+ return {
280
+ debut: `${base}-debut`,
281
+ fin: `${base}-fin`,
282
+ calendrier: `${base}-calendrier`,
283
+ };
284
+ })();
285
+ calendrier = viewChild(StCalendar, /* @ts-ignore */
286
+ ...(ngDevMode ? [{ debugName: "calendrier" }] : /* istanbul ignore next */ []));
287
+ /**
288
+ * Quitte au moins une fois. Relie a un formulaire, `touched` le dit ; employe
289
+ * seul, rien ne le dirait, et une saisie illisible ne s'afficherait jamais.
290
+ */
291
+ quitte = signal(false, /* @ts-ignore */
292
+ ...(ngDevMode ? [{ debugName: "quitte" }] : /* istanbul ignore next */ []));
293
+ saisie = transformedValue(this.value, {
294
+ parse: (brut) => {
295
+ const debut = this.lirePartie(brut.debut);
296
+ const fin = this.lirePartie(brut.fin);
297
+ const erreurs = [];
298
+ if (debut === undefined)
299
+ erreurs.push({ kind: 'date', partie: 'debut' });
300
+ if (fin === undefined)
301
+ erreurs.push({ kind: 'date', partie: 'fin' });
302
+ if (debut === undefined || fin === undefined)
303
+ return { error: erreurs };
304
+ return { value: { debut, fin } };
305
+ },
306
+ format: (plage) => ({ debut: this.formater(plage.debut), fin: this.formater(plage.fin) }),
307
+ });
308
+ etiquettes = computed(() => ({
309
+ debut: this.libelleDebut() ?? this.libelles().dateDeDebut,
310
+ fin: this.libelleFin() ?? this.libelles().dateDeFin,
311
+ }), /* @ts-ignore */
312
+ ...(ngDevMode ? [{ debugName: "etiquettes" }] : /* istanbul ignore next */ []));
313
+ aideLue = computed(() => {
314
+ const libelles = this.libelles();
315
+ const locale = this.locale();
316
+ const format = libelles.formatDeDate(motifLocal(libelles, locale), exempleDeDate(locale));
317
+ const aide = this.aide();
318
+ return aide === null ? format : `${aide} ${format}`;
319
+ }, /* @ts-ignore */
320
+ ...(ngDevMode ? [{ debugName: "aideLue" }] : /* istanbul ignore next */ []));
321
+ /** Chaque erreur prefixee de sa partie, dans la langue courante. */
322
+ erreursAffichees = computed(() => {
323
+ const libelles = this.libelles();
324
+ const locale = this.locale();
325
+ const etiquettes = this.etiquettes();
326
+ return fusionnerErreurs(this.errors(), this.saisie.parseErrors()).map((erreur) => {
327
+ const partie = this.partieDe(erreur);
328
+ if (partie === null)
329
+ return erreur;
330
+ return {
331
+ kind: erreur.kind,
332
+ message: `${etiquettes[partie]}\u00a0: ${messageDErreur(erreur, libelles, locale)}`,
333
+ };
334
+ });
335
+ }, /* @ts-ignore */
336
+ ...(ngDevMode ? [{ debugName: "erreursAffichees" }] : /* istanbul ignore next */ []));
337
+ invalide = computed(() => {
338
+ const erreurs = this.touched() || this.quitte()
339
+ ? fusionnerErreurs(this.errors(), this.saisie.parseErrors())
340
+ : [];
341
+ return {
342
+ debut: erreurs.some((erreur) => this.partieDe(erreur) === 'debut'),
343
+ fin: erreurs.some((erreur) => this.partieDe(erreur) === 'fin'),
344
+ };
345
+ }, /* @ts-ignore */
346
+ ...(ngDevMode ? [{ debugName: "invalide" }] : /* istanbul ignore next */ []));
347
+ surSaisie(partie, evenement) {
348
+ this.saisie.set({ ...this.saisie(), [partie]: this.texteSaisi(evenement) });
349
+ }
350
+ aLaSortieDuChamp() {
351
+ if (this.saisie.parseErrors().length === 0) {
352
+ const plage = this.value();
353
+ this.saisie.set({ debut: this.formater(plage.debut), fin: this.formater(plage.fin) });
354
+ }
355
+ }
356
+ basculer(partie) {
357
+ if (this.ouvert() === partie) {
358
+ this.ouvert.set(null);
359
+ return;
360
+ }
361
+ this.ouvert.set(partie);
362
+ afterNextRender(() => {
363
+ this.calendrier()?.focus();
364
+ }, { injector: this.injecteur });
365
+ }
366
+ bornes(partie) {
367
+ const plage = this.value();
368
+ return partie === 'debut'
369
+ ? { min: this.dateMin(), max: plage.fin ?? this.dateMax() }
370
+ : { min: plage.debut ?? this.dateMin(), max: this.dateMax() };
371
+ }
372
+ choisir(partie, date) {
373
+ this.value.set({ ...this.value(), [partie]: date });
374
+ this.fermer(partie);
375
+ }
376
+ fermer(partie) {
377
+ this.ouvert.set(null);
378
+ afterNextRender(() => {
379
+ this.element.nativeElement
380
+ .querySelector(`#${this.identifiants[partie]}`)
381
+ ?.focus();
382
+ }, { injector: this.injecteur });
383
+ }
384
+ aLaSortieDuComposant(evenement) {
385
+ const suivant = evenement.relatedTarget;
386
+ if (suivant !== null && this.element.nativeElement.contains(suivant))
387
+ return;
388
+ this.ouvert.set(null);
389
+ this.quitte.set(true);
390
+ this.touch.emit();
391
+ }
392
+ /** `null` : vide. `undefined` : illisible. */
393
+ lirePartie(texte) {
394
+ if (texte.trim() === '')
395
+ return null;
396
+ return lireDate(texte, this.locale()) ?? undefined;
397
+ }
398
+ formater(valeur) {
399
+ return valeur === null ? '' : formaterDateSaisie(valeur, this.locale());
400
+ }
401
+ /** La partie visee : celle d'une erreur de lecture, la fin pour une plage inversee. */
402
+ partieDe(erreur) {
403
+ const partie = erreur.partie;
404
+ if (partie === 'debut' || partie === 'fin')
405
+ return partie;
406
+ return erreur.kind === 'plageDeDates' ? 'fin' : null;
407
+ }
408
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDateRange, deps: null, target: i0.ɵɵFactoryTarget.Component });
409
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StDateRange, isStandalone: true, selector: "st-date-range", inputs: { value: { classPropertyName: "value", publicName: "value", 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 }, libelleDebut: { classPropertyName: "libelleDebut", publicName: "libelleDebut", isSignal: true, isRequired: false, transformFunction: null }, libelleFin: { classPropertyName: "libelleFin", publicName: "libelleFin", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { listeners: { "focusout": "aLaSortieDuComposant($event)" }, classAttribute: "st-date-range" }, viewQueries: [{ propertyName: "calendrier", first: true, predicate: StCalendar, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<st-fieldset\n [legende]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"erreursAffichees()\"\n [afficherErreurs]=\"touched() || quitte()\"\n [obligatoire]=\"required()\"\n>\n <div class=\"st-date-range__parties\">\n @for (partie of parties; track partie) {\n <div class=\"st-date-range__partie\">\n <label class=\"st-date-range__libelle\" [for]=\"identifiants[partie]\">\n {{ etiquettes()[partie] }}\n </label>\n <div class=\"st-date-range__ligne\">\n <input\n #champ\n class=\"st-date-range__champ\"\n type=\"text\"\n spellcheck=\"false\"\n [attr.data-libelle]=\"etiquettes()[partie]\"\n [id]=\"identifiants[partie]\"\n [value]=\"saisie()[partie]\"\n [attr.name]=\"name() ? name() + '-' + partie : null\"\n [attr.aria-invalid]=\"invalide()[partie] ? 'true' : null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie(partie, $event)\"\n (blur)=\"aLaSortieDuChamp()\"\n />\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=\"calendrier\"\n [etiquette]=\"libelles().choisirDansLeCalendrier(etiquettes()[partie])\"\n [attr.aria-expanded]=\"ouvert() === partie\"\n [attr.aria-controls]=\"ouvert() === partie ? identifiants.calendrier : null\"\n [disabled]=\"disabled() || readonly()\"\n (click)=\"basculer(partie)\"\n ></button>\n </div>\n </div>\n }\n </div>\n @if (ouvert(); as partie) {\n <st-calendar\n class=\"st-date-range__calendrier\"\n [id]=\"identifiants.calendrier\"\n [selection]=\"value()[partie]\"\n [plage]=\"value()\"\n [dateMin]=\"bornes(partie).min\"\n [dateMax]=\"bornes(partie).max\"\n (choix)=\"choisir(partie, $event)\"\n (fermeture)=\"fermer(partie)\"\n />\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-date-range__parties{display:flex;flex-wrap:wrap;gap:var(--st-espacement-3) var(--st-espacement-5)}.st-date-range__libelle{display:block;margin-block-end:var(--st-espacement-1);font-weight:var(--st-graisse-semi)}.st-date-range__ligne{display:flex;gap:var(--st-espacement-1);align-items:center}.st-date-range__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-date-range__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-date-range__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-date-range__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-date-range__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-date-range__champ{max-inline-size:12rem;font-variant-numeric:tabular-nums}.st-date-range__calendrier{margin-block-start:var(--st-espacement-3)}\n"], dependencies: [{ kind: "component", type: StFieldset, selector: "st-fieldset", inputs: ["legende", "aide", "erreurs", "afficherErreurs", "obligatoire"] }, { kind: "component", type: StIconButton, selector: "button[stIconButton]", inputs: ["icone", "etiquette", "variante", "taille", "enfonce"] }, { kind: "component", type: StCalendar, selector: "st-calendar", inputs: ["selection", "dateMin", "dateMax", "plage"], outputs: ["choix", "fermeture"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
410
+ }
411
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDateRange, decorators: [{
412
+ type: Component,
413
+ args: [{ selector: 'st-date-range', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StFieldset, StIconButton, StCalendar], host: {
414
+ class: 'st-date-range',
415
+ '(focusout)': 'aLaSortieDuComposant($event)',
416
+ }, template: "<st-fieldset\n [legende]=\"libelle()\"\n [aide]=\"aideLue()\"\n [erreurs]=\"erreursAffichees()\"\n [afficherErreurs]=\"touched() || quitte()\"\n [obligatoire]=\"required()\"\n>\n <div class=\"st-date-range__parties\">\n @for (partie of parties; track partie) {\n <div class=\"st-date-range__partie\">\n <label class=\"st-date-range__libelle\" [for]=\"identifiants[partie]\">\n {{ etiquettes()[partie] }}\n </label>\n <div class=\"st-date-range__ligne\">\n <input\n #champ\n class=\"st-date-range__champ\"\n type=\"text\"\n spellcheck=\"false\"\n [attr.data-libelle]=\"etiquettes()[partie]\"\n [id]=\"identifiants[partie]\"\n [value]=\"saisie()[partie]\"\n [attr.name]=\"name() ? name() + '-' + partie : null\"\n [attr.aria-invalid]=\"invalide()[partie] ? 'true' : null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"surSaisie(partie, $event)\"\n (blur)=\"aLaSortieDuChamp()\"\n />\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=\"calendrier\"\n [etiquette]=\"libelles().choisirDansLeCalendrier(etiquettes()[partie])\"\n [attr.aria-expanded]=\"ouvert() === partie\"\n [attr.aria-controls]=\"ouvert() === partie ? identifiants.calendrier : null\"\n [disabled]=\"disabled() || readonly()\"\n (click)=\"basculer(partie)\"\n ></button>\n </div>\n </div>\n }\n </div>\n @if (ouvert(); as partie) {\n <st-calendar\n class=\"st-date-range__calendrier\"\n [id]=\"identifiants.calendrier\"\n [selection]=\"value()[partie]\"\n [plage]=\"value()\"\n [dateMin]=\"bornes(partie).min\"\n [dateMax]=\"bornes(partie).max\"\n (choix)=\"choisir(partie, $event)\"\n (fermeture)=\"fermer(partie)\"\n />\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-date-range__parties{display:flex;flex-wrap:wrap;gap:var(--st-espacement-3) var(--st-espacement-5)}.st-date-range__libelle{display:block;margin-block-end:var(--st-espacement-1);font-weight:var(--st-graisse-semi)}.st-date-range__ligne{display:flex;gap:var(--st-espacement-1);align-items:center}.st-date-range__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-date-range__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-date-range__champ[aria-invalid=true]{border-color:var(--st-couleur-danger);box-shadow:inset 0 0 0 1px var(--st-couleur-danger)}.st-date-range__champ:disabled{color:var(--st-couleur-texte-desactive);cursor:not-allowed;background:var(--st-couleur-surface-enfoncee)}.st-date-range__champ:read-only:not(:disabled){background:var(--st-couleur-surface-enfoncee)}.st-date-range__champ{max-inline-size:12rem;font-variant-numeric:tabular-nums}.st-date-range__calendrier{margin-block-start:var(--st-espacement-3)}\n"] }]
417
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], dateMin: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateMin", required: false }] }], dateMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateMax", required: false }] }], libelleDebut: [{ type: i0.Input, args: [{ isSignal: true, alias: "libelleDebut", required: false }] }], libelleFin: [{ type: i0.Input, args: [{ isSignal: true, alias: "libelleFin", required: false }] }], calendrier: [{ type: i0.ViewChild, args: [i0.forwardRef(() => StCalendar), { isSignal: true }] }] } });
418
+
419
+ /**
420
+ * @starterlib/ui/formulaires — les champs qui LISENT leur saisie : montant, date,
421
+ * plage de dates.
422
+ *
423
+ * ⚠️ Un point d'entree separe pour une raison de dependance (ADR 0003). « 12,5,3 »
424
+ * n'est aucun montant, « 31/02/2026 » aucune date : le formulaire doit le savoir,
425
+ * sans quoi il envoie l'ancienne valeur en croyant la nouvelle. Signal Forms le
426
+ * permet par `transformedValue`, qui est du CODE d'`@angular/forms` — la ou le
427
+ * tronc n'importe que des types. Laisse dans le tronc, `@angular/forms` deviendrait
428
+ * obligatoire pour toute application qui affiche un bouton.
429
+ *
430
+ * Rien ici n'importe le CDK ni Aria : le calendrier vient du tronc, et une
431
+ * application qui saisit un montant ne charge pas de quoi ouvrir une liste.
432
+ *
433
+ * ⚠️ En federation, ce point d'entree se partage SEPAREMENT : c'est un
434
+ * specificateur de module distinct.
435
+ *
436
+ * ```ts
437
+ * import { StAmountField, StDateField } from '@starterlib/ui/formulaires';
438
+ * ```
439
+ */
440
+
441
+ /**
442
+ * Generated bundle index. Do not edit.
443
+ */
444
+
445
+ export { StAmountField, StDateField, StDateRange };
446
+ //# sourceMappingURL=starterlib-ui-formulaires.mjs.map