@starterlib/ui 0.2.0 → 0.3.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,12 +1,18 @@
1
1
  import { Overlay } from '@angular/cdk/overlay';
2
- import { DomPortal } from '@angular/cdk/portal';
2
+ import { DomPortal, CdkPortalOutlet, ComponentPortal } from '@angular/cdk/portal';
3
3
  import * as i0 from '@angular/core';
4
- import { inject, ElementRef, input, signal, DestroyRef, ChangeDetectionStrategy, Component, Directive } from '@angular/core';
4
+ import { inject, ElementRef, input, signal, DestroyRef, ChangeDetectionStrategy, Component, Directive, InjectionToken, afterNextRender, computed, Injectable, effect, output } from '@angular/core';
5
5
  import * as i1 from '@angular/aria/menu';
6
6
  import { Menu, MenuItem } from '@angular/aria/menu';
7
- import { StIcon } from '@starterlib/ui';
7
+ import { StIcon, ST_LIBELLES, StButton, StIconButton, ST_LOCALE, AnnonceLocale, formaterDuree } from '@starterlib/ui';
8
+ import { CdkDialogContainer, DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
9
+ export { DIALOG_DATA as DONNEES_DE_DIALOGUE } from '@angular/cdk/dialog';
10
+ import { hasModifierKey } from '@angular/cdk/keycodes';
11
+ import { Router, NavigationStart } from '@angular/router';
12
+ import { AriaDescriber } from '@angular/cdk/a11y';
13
+ import { DOCUMENT } from '@angular/common';
8
14
 
9
- const POSITIONS = {
15
+ const POSITIONS$2 = {
10
16
  debut: [
11
17
  { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
12
18
  { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
@@ -76,7 +82,7 @@ class StMenu {
76
82
  positionStrategy: this.overlay
77
83
  .position()
78
84
  .flexibleConnectedTo(declencheur)
79
- .withPositions(POSITIONS[this.alignement()])
85
+ .withPositions(POSITIONS$2[this.alignement()])
80
86
  .withPush(true),
81
87
  // Le menu suit son declencheur au defilement : le figer le laisserait
82
88
  // seul au milieu de l'ecran, sans lien visible avec ce qu'il commande.
@@ -125,7 +131,7 @@ class StMenu {
125
131
  this.couche = null;
126
132
  }
127
133
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
128
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StMenu, isStandalone: true, selector: "st-menu", inputs: { etiquette: { classPropertyName: "etiquette", publicName: "etiquette", isSignal: true, isRequired: true, transformFunction: null }, alignement: { classPropertyName: "alignement", publicName: "alignement", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "auClavier($event)" }, properties: { "attr.aria-label": "etiquette()", "attr.data-ouvert": "ouvert() ? \"\" : null" }, classAttribute: "st-menu" }, hostDirectives: [{ directive: i1.Menu, inputs: ["wrap", "wrap", "typeaheadDelay", "typeaheadDelay", "disabled", "disabled"], outputs: ["itemSelected", "selection"] }], ngImport: i0, template: "<ng-content />\n", styles: [":host{display:none}:host([data-ouvert]){display:flex;flex-direction:column;gap:2px;min-inline-size:12rem;max-block-size:80vh;padding:var(--st-espacement-1);overflow-y:auto;background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-2)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
134
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StMenu, isStandalone: true, selector: "st-menu", inputs: { etiquette: { classPropertyName: "etiquette", publicName: "etiquette", isSignal: true, isRequired: true, transformFunction: null }, alignement: { classPropertyName: "alignement", publicName: "alignement", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "auClavier($event)" }, properties: { "attr.aria-label": "etiquette()", "attr.data-ouvert": "ouvert() ? \"\" : null" }, classAttribute: "st-menu" }, hostDirectives: [{ directive: i1.Menu, inputs: ["wrap", "wrap", "typeaheadDelay", "typeaheadDelay", "disabled", "disabled"], outputs: ["itemSelected", "selection"] }], ngImport: i0, template: "<ng-content />\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:none}:host([data-ouvert]){display:flex;flex-direction:column;gap:2px;min-inline-size:12rem;max-block-size:80vh;padding:var(--st-espacement-1);overflow-y:auto;background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-2)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
129
135
  }
130
136
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenu, decorators: [{
131
137
  type: Component,
@@ -140,7 +146,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
140
146
  '[attr.aria-label]': 'etiquette()',
141
147
  '[attr.data-ouvert]': 'ouvert() ? "" : null',
142
148
  '(keydown)': 'auClavier($event)',
143
- }, template: "<ng-content />\n", styles: [":host{display:none}:host([data-ouvert]){display:flex;flex-direction:column;gap:2px;min-inline-size:12rem;max-block-size:80vh;padding:var(--st-espacement-1);overflow-y:auto;background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-2)}\n"] }]
149
+ }, template: "<ng-content />\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:none}:host([data-ouvert]){display:flex;flex-direction:column;gap:2px;min-inline-size:12rem;max-block-size:80vh;padding:var(--st-espacement-1);overflow-y:auto;background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-2)}\n"] }]
144
150
  }], ctorParameters: () => [], propDecorators: { etiquette: [{ type: i0.Input, args: [{ isSignal: true, alias: "etiquette", required: true }] }], alignement: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignement", required: false }] }] } });
145
151
 
146
152
  /**
@@ -163,7 +169,7 @@ class StMenuItem {
163
169
  ton = input('neutre', /* @ts-ignore */
164
170
  ...(ngDevMode ? [{ debugName: "ton" }] : /* istanbul ignore next */ []));
165
171
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
166
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StMenuItem, isStandalone: true, selector: "button[stMenuItem]", inputs: { icone: { classPropertyName: "icone", publicName: "icone", isSignal: true, isRequired: false, transformFunction: null }, ton: { classPropertyName: "ton", publicName: "ton", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button" }, properties: { "attr.data-ton": "ton()" }, classAttribute: "st-menu-item" }, hostDirectives: [{ directive: i1.MenuItem, inputs: ["value", "valeur", "disabled", "desactive", "role", "role"] }], ngImport: i0, template: "@if (icone(); as nom) {\n <st-icon [nom]=\"nom\" taille=\"sm\" />\n}\n\n<span class=\"st-menu-item__libelle\"><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{display:flex;gap:var(--st-espacement-2);align-items:center;inline-size:100%;min-block-size:2.25rem;padding-block:var(--st-espacement-2);padding-inline:var(--st-espacement-3);font-family:var(--st-police-base);font-size:var(--st-taille-sm);color:var(--st-couleur-texte);text-align:start;cursor:pointer;background:transparent;border:0;border-radius:var(--st-rayon-sm)}:host:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:-2px}:host:hover:not([aria-disabled=true]){background:var(--st-couleur-surface-survol)}:host[data-active=true]{background:var(--st-couleur-surface-survol)}:host[aria-disabled=true]{color:var(--st-couleur-texte-desactive);cursor:not-allowed}@media(any-pointer:coarse){:host{min-block-size:var(--st-cible-tactile-min)}}:host([data-ton=danger]){color:var(--st-couleur-danger)}:host([data-ton=danger]):hover:not([aria-disabled=true]),:host([data-ton=danger])[data-active=true]{background:var(--st-couleur-danger-discret)}.st-menu-item__libelle{flex:1}\n"], dependencies: [{ kind: "component", type: StIcon, selector: "st-icon", inputs: ["nom", "taille", "ton", "etiquette"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
172
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StMenuItem, isStandalone: true, selector: "button[stMenuItem]", inputs: { icone: { classPropertyName: "icone", publicName: "icone", isSignal: true, isRequired: false, transformFunction: null }, ton: { classPropertyName: "ton", publicName: "ton", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button" }, properties: { "attr.data-ton": "ton()" }, classAttribute: "st-menu-item" }, hostDirectives: [{ directive: i1.MenuItem, inputs: ["value", "valeur", "disabled", "desactive", "role", "role"] }], ngImport: i0, template: "@if (icone(); as nom) {\n <st-icon [nom]=\"nom\" taille=\"sm\" />\n}\n\n<span class=\"st-menu-item__libelle\"><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:center;inline-size:100%;min-block-size:2.25rem;padding-block:var(--st-espacement-2);padding-inline:var(--st-espacement-3);font-family:var(--st-police-base);font-size:var(--st-taille-sm);color:var(--st-couleur-texte);text-align:start;cursor:pointer;background:transparent;border:0;border-radius:var(--st-rayon-sm)}:host:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:-2px}:host:hover:not([aria-disabled=true]){background:var(--st-couleur-surface-survol)}:host[data-active=true]{background:var(--st-couleur-surface-survol)}:host[aria-disabled=true]{color:var(--st-couleur-texte-desactive);cursor:not-allowed}@media(any-pointer:coarse){:host{min-block-size:var(--st-cible-tactile-min)}}:host([data-ton=danger]){color:var(--st-couleur-danger)}:host([data-ton=danger]):hover:not([aria-disabled=true]),:host([data-ton=danger])[data-active=true]{background:var(--st-couleur-danger-discret)}.st-menu-item__libelle{flex:1}\n"], dependencies: [{ kind: "component", type: StIcon, selector: "st-icon", inputs: ["nom", "taille", "ton", "etiquette"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
167
173
  }
168
174
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenuItem, decorators: [{
169
175
  type: Component,
@@ -173,7 +179,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
173
179
  class: 'st-menu-item',
174
180
  type: 'button',
175
181
  '[attr.data-ton]': 'ton()',
176
- }, template: "@if (icone(); as nom) {\n <st-icon [nom]=\"nom\" taille=\"sm\" />\n}\n\n<span class=\"st-menu-item__libelle\"><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{display:flex;gap:var(--st-espacement-2);align-items:center;inline-size:100%;min-block-size:2.25rem;padding-block:var(--st-espacement-2);padding-inline:var(--st-espacement-3);font-family:var(--st-police-base);font-size:var(--st-taille-sm);color:var(--st-couleur-texte);text-align:start;cursor:pointer;background:transparent;border:0;border-radius:var(--st-rayon-sm)}:host:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:-2px}:host:hover:not([aria-disabled=true]){background:var(--st-couleur-surface-survol)}:host[data-active=true]{background:var(--st-couleur-surface-survol)}:host[aria-disabled=true]{color:var(--st-couleur-texte-desactive);cursor:not-allowed}@media(any-pointer:coarse){:host{min-block-size:var(--st-cible-tactile-min)}}:host([data-ton=danger]){color:var(--st-couleur-danger)}:host([data-ton=danger]):hover:not([aria-disabled=true]),:host([data-ton=danger])[data-active=true]{background:var(--st-couleur-danger-discret)}.st-menu-item__libelle{flex:1}\n"] }]
182
+ }, template: "@if (icone(); as nom) {\n <st-icon [nom]=\"nom\" taille=\"sm\" />\n}\n\n<span class=\"st-menu-item__libelle\"><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:center;inline-size:100%;min-block-size:2.25rem;padding-block:var(--st-espacement-2);padding-inline:var(--st-espacement-3);font-family:var(--st-police-base);font-size:var(--st-taille-sm);color:var(--st-couleur-texte);text-align:start;cursor:pointer;background:transparent;border:0;border-radius:var(--st-rayon-sm)}:host:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:-2px}:host:hover:not([aria-disabled=true]){background:var(--st-couleur-surface-survol)}:host[data-active=true]{background:var(--st-couleur-surface-survol)}:host[aria-disabled=true]{color:var(--st-couleur-texte-desactive);cursor:not-allowed}@media(any-pointer:coarse){:host{min-block-size:var(--st-cible-tactile-min)}}:host([data-ton=danger]){color:var(--st-couleur-danger)}:host([data-ton=danger]):hover:not([aria-disabled=true]),:host([data-ton=danger])[data-active=true]{background:var(--st-couleur-danger-discret)}.st-menu-item__libelle{flex:1}\n"] }]
177
183
  }], propDecorators: { icone: [{ type: i0.Input, args: [{ isSignal: true, alias: "icone", required: false }] }], ton: [{ type: i0.Input, args: [{ isSignal: true, alias: "ton", required: false }] }] } });
178
184
 
179
185
  /**
@@ -219,6 +225,1092 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
219
225
  }]
220
226
  }], propDecorators: { menu: [{ type: i0.Input, args: [{ isSignal: true, alias: "menu", required: true }] }] } });
221
227
 
228
+ /**
229
+ * Identifiant du titre du dialogue, decide a l'ouverture et fourni au contenu.
230
+ *
231
+ * ⚠️ Il est engendre par le service et non par le composant de titre : c'est la
232
+ * configuration du dialogue qui doit porter `aria-labelledby`, et elle est
233
+ * figee a l'ouverture. Un identifiant decide plus tard arriverait apres que le
234
+ * conteneur a lu sa configuration, et le dialogue s'ouvrirait sans nom.
235
+ */
236
+ const IDENTIFIANT_DU_TITRE = new InjectionToken('st.dialogue.identifiant-du-titre');
237
+
238
+ /**
239
+ * Le panneau du dialogue : sa surface, son arrondi, son ombre.
240
+ *
241
+ * ⚠️ Pourquoi ce composant existe. `CdkDialogContainer` n'a **aucune
242
+ * apparence** : c'est voulu de la part du CDK, qui ne decide d'aucun style. Le
243
+ * dialogue fonctionnait donc parfaitement — role, nom, piegeage du focus,
244
+ * Echap — tout en s'affichant SANS FOND, le texte flottant par-dessus la page.
245
+ * Aucun test ne pouvait le voir : jsdom ne calcule pas la mise en page, et les
246
+ * tests verifiaient le comportement, pas l'aspect.
247
+ *
248
+ * ⚠️ Un conteneur maison plutot qu'une feuille de style globale a importer :
249
+ * une SPA qui oublierait l'import retrouverait exactement le meme defaut, et
250
+ * l'oubli ne se verrait qu'a l'ecran. Ici, l'apparence voyage avec le
251
+ * composant.
252
+ *
253
+ * L'encapsulation reste EMULEE : seuls l'hote et le panneau qui l'enveloppe
254
+ * sont styles, par `:host` et `:host-context()`. Aucun contenu projete n'est
255
+ * vise, donc rien ne justifie de rendre ces styles globaux.
256
+ */
257
+ class StDialogContainer extends CdkDialogContainer {
258
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogContainer, deps: null, target: i0.ɵɵFactoryTarget.Component });
259
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: StDialogContainer, isStandalone: true, selector: "st-dialog-container", host: { attributes: { "tabindex": "-1" }, properties: { "attr.role": "_config.role", "attr.aria-modal": "_config.ariaModal", "attr.aria-labelledby": "_config.ariaLabel ? null : _config.ariaLabelledBy", "attr.aria-label": "_config.ariaLabel", "attr.aria-describedby": "_config.ariaDescribedBy || null" }, classAttribute: "st-dialog-container" }, usesInheritance: true, ngImport: i0, template: "<ng-template cdkPortalOutlet />\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;flex-direction:column;max-block-size:inherit;overflow:hidden;color:var(--st-couleur-texte);background:var(--st-couleur-surface-elevee);border-radius:var(--st-rayon-lg);box-shadow:var(--st-ombre-xl)}:host(:focus){outline:none}:host-context(.st-tiroir){block-size:100%;max-block-size:100%;border-radius:0}:host-context(.st-tiroir--bas){border-start-start-radius:var(--st-rayon-lg);border-start-end-radius:var(--st-rayon-lg)}@media(forced-colors:active){:host{border:1px solid CanvasText}}\n"], dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
260
+ }
261
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogContainer, decorators: [{
262
+ type: Component,
263
+ args: [{ selector: 'st-dialog-container', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CdkPortalOutlet], host: {
264
+ class: 'st-dialog-container',
265
+ '[attr.role]': '_config.role',
266
+ '[attr.aria-modal]': '_config.ariaModal',
267
+ '[attr.aria-labelledby]': '_config.ariaLabel ? null : _config.ariaLabelledBy',
268
+ '[attr.aria-label]': '_config.ariaLabel',
269
+ '[attr.aria-describedby]': '_config.ariaDescribedBy || null',
270
+ tabindex: '-1',
271
+ }, template: "<ng-template cdkPortalOutlet />\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;flex-direction:column;max-block-size:inherit;overflow:hidden;color:var(--st-couleur-texte);background:var(--st-couleur-surface-elevee);border-radius:var(--st-rayon-lg);box-shadow:var(--st-ombre-xl)}:host(:focus){outline:none}:host-context(.st-tiroir){block-size:100%;max-block-size:100%;border-radius:0}:host-context(.st-tiroir--bas){border-start-start-radius:var(--st-rayon-lg);border-start-end-radius:var(--st-rayon-lg)}@media(forced-colors:active){:host{border:1px solid CanvasText}}\n"] }]
272
+ }] });
273
+
274
+ /**
275
+ * Titre du dialogue. Il porte l'identifiant que la configuration a designe en
276
+ * `aria-labelledby` : c'est lui qui NOMME le dialogue.
277
+ *
278
+ * ⚠️ Un `h2` et non un `h1` : le dialogue s'ouvre par-dessus une page qui a
279
+ * deja son titre de niveau 1, et deux `h1` cassent la table des matieres que
280
+ * les lecteurs d'ecran construisent.
281
+ */
282
+ class StDialogTitle {
283
+ identifiant = inject(IDENTIFIANT_DU_TITRE, { optional: true });
284
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogTitle, deps: [], target: i0.ɵɵFactoryTarget.Component });
285
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: StDialogTitle, isStandalone: true, selector: "st-dialog-title", host: { classAttribute: "st-dialog-title" }, ngImport: i0, template: "<h2 class=\"st-dialog-title__texte\" [attr.id]=\"identifiant\"><ng-content /></h2>\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;padding:var(--st-espacement-5) var(--st-espacement-5) 0}.st-dialog-title__texte{margin:0;font-family:var(--st-police-base);font-size:var(--st-taille-xl);font-weight:var(--st-graisse-semi);line-height:var(--st-interligne-serre);color:var(--st-couleur-texte);text-wrap:balance}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
286
+ }
287
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogTitle, decorators: [{
288
+ type: Component,
289
+ args: [{ selector: 'st-dialog-title', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'st-dialog-title' }, template: "<h2 class=\"st-dialog-title__texte\" [attr.id]=\"identifiant\"><ng-content /></h2>\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;padding:var(--st-espacement-5) var(--st-espacement-5) 0}.st-dialog-title__texte{margin:0;font-family:var(--st-police-base);font-size:var(--st-taille-xl);font-weight:var(--st-graisse-semi);line-height:var(--st-interligne-serre);color:var(--st-couleur-texte);text-wrap:balance}\n"] }]
290
+ }] });
291
+
292
+ /**
293
+ * Corps du dialogue, defilant quand il deborde.
294
+ *
295
+ * ⚠️ Le defilement est ICI et non sur le dialogue entier : titre et actions
296
+ * doivent rester visibles. Un dialogue long dont les boutons partent hors de
297
+ * l'ecran est le defaut classique, et il ne se voit qu'a 200 % d'echelle ou sur
298
+ * un ecran bas.
299
+ *
300
+ * ⚠️ **Focalisable seulement quand il defile.** Une zone defilante sans
301
+ * controle doit recevoir le focus, sinon le clavier ne peut pas la faire
302
+ * defiler — le texte d'une condition generale, par exemple. Mais la premiere
303
+ * version posait `tabindex="0"` sans condition : sur un dialogue court, la zone
304
+ * de texte devenait le premier arret de tabulation, AVANT le bouton
305
+ * d'annulation, contrairement a ce que `StConfirmDialog` affirmait.
306
+ */
307
+ class StDialogContent {
308
+ /** `aucun` pour un contenu qui gere ses propres marges, comme un tableau. */
309
+ rembourrage = input('md', /* @ts-ignore */
310
+ ...(ngDevMode ? [{ debugName: "rembourrage" }] : /* istanbul ignore next */ []));
311
+ defilante = signal(false, /* @ts-ignore */
312
+ ...(ngDevMode ? [{ debugName: "defilante" }] : /* istanbul ignore next */ []));
313
+ constructor() {
314
+ const element = inject(ElementRef).nativeElement;
315
+ const destruction = inject(DestroyRef);
316
+ // Mesure apres le rendu, puis a chaque changement de taille ou de contenu :
317
+ // des donnees arrivees apres l'ouverture peuvent faire deborder un dialogue
318
+ // qui ne debordait pas.
319
+ afterNextRender(() => {
320
+ const mesurer = () => {
321
+ this.defilante.set(element.scrollHeight > element.clientHeight + 1);
322
+ };
323
+ mesurer();
324
+ const observateurs = [];
325
+ if (typeof ResizeObserver !== 'undefined') {
326
+ const taille = new ResizeObserver(mesurer);
327
+ taille.observe(element);
328
+ observateurs.push(taille);
329
+ }
330
+ if (typeof MutationObserver !== 'undefined') {
331
+ const contenu = new MutationObserver(mesurer);
332
+ contenu.observe(element, { childList: true, subtree: true, characterData: true });
333
+ observateurs.push(contenu);
334
+ }
335
+ destruction.onDestroy(() => {
336
+ for (const observateur of observateurs)
337
+ observateur.disconnect();
338
+ });
339
+ });
340
+ }
341
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogContent, deps: [], target: i0.ɵɵFactoryTarget.Component });
342
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StDialogContent, isStandalone: true, selector: "st-dialog-content", inputs: { rembourrage: { classPropertyName: "rembourrage", publicName: "rembourrage", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.tabindex": "defilante() ? 0 : null", "attr.data-rembourrage": "rembourrage()" }, classAttribute: "st-dialog-content" }, ngImport: i0, template: "<ng-content />\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:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:-2px}:host{display:block;overflow-y:auto;max-block-size:60vh;color:var(--st-couleur-texte)}:host([data-rembourrage=md]){padding:var(--st-espacement-4) var(--st-espacement-5)}:host([data-rembourrage=aucun]){padding:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
343
+ }
344
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogContent, decorators: [{
345
+ type: Component,
346
+ args: [{ selector: 'st-dialog-content', changeDetection: ChangeDetectionStrategy.OnPush, host: {
347
+ class: 'st-dialog-content',
348
+ '[attr.tabindex]': 'defilante() ? 0 : null',
349
+ '[attr.data-rembourrage]': 'rembourrage()',
350
+ }, template: "<ng-content />\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:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:-2px}:host{display:block;overflow-y:auto;max-block-size:60vh;color:var(--st-couleur-texte)}:host([data-rembourrage=md]){padding:var(--st-espacement-4) var(--st-espacement-5)}:host([data-rembourrage=aucun]){padding:0}\n"] }]
351
+ }], ctorParameters: () => [], propDecorators: { rembourrage: [{ type: i0.Input, args: [{ isSignal: true, alias: "rembourrage", required: false }] }] } });
352
+
353
+ /**
354
+ * Barre d'actions du dialogue.
355
+ *
356
+ * ⚠️ L'action de confirmation est a DROITE et l'annulation a gauche, dans cet
357
+ * ordre dans le DOM : l'ordre de tabulation doit suivre l'ordre visuel, sinon
358
+ * le clavier et la souris ne donnent pas le meme parcours. Inverser
359
+ * visuellement par `row-reverse` casserait cette correspondance.
360
+ */
361
+ class StDialogActions {
362
+ alignement = input('fin', /* @ts-ignore */
363
+ ...(ngDevMode ? [{ debugName: "alignement" }] : /* istanbul ignore next */ []));
364
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogActions, deps: [], target: i0.ɵɵFactoryTarget.Component });
365
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StDialogActions, isStandalone: true, selector: "st-dialog-actions", inputs: { alignement: { classPropertyName: "alignement", publicName: "alignement", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-alignement": "alignement()" }, classAttribute: "st-dialog-actions" }, ngImport: i0, template: "<ng-content />\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;flex-wrap:wrap;gap:var(--st-espacement-2);align-items:center;padding:var(--st-espacement-4) var(--st-espacement-5) var(--st-espacement-5)}:host([data-alignement=fin]){justify-content:flex-end}:host([data-alignement=debut]){justify-content:flex-start}:host([data-alignement=entre]){justify-content:space-between}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
366
+ }
367
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogActions, decorators: [{
368
+ type: Component,
369
+ args: [{ selector: 'st-dialog-actions', changeDetection: ChangeDetectionStrategy.OnPush, host: {
370
+ class: 'st-dialog-actions',
371
+ '[attr.data-alignement]': 'alignement()',
372
+ }, template: "<ng-content />\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;flex-wrap:wrap;gap:var(--st-espacement-2);align-items:center;padding:var(--st-espacement-4) var(--st-espacement-5) var(--st-espacement-5)}:host([data-alignement=fin]){justify-content:flex-end}:host([data-alignement=debut]){justify-content:flex-start}:host([data-alignement=entre]){justify-content:space-between}\n"] }]
373
+ }], propDecorators: { alignement: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignement", required: false }] }] } });
374
+
375
+ let compteur$3 = 0;
376
+ /**
377
+ * Dialogue de confirmation.
378
+ *
379
+ * ⚠️ **Le bouton de confirmation n'est jamais celui qui recoit le focus.** Le
380
+ * dialogue entier le recoit (voir `StDialog`), et le premier arret de
381
+ * tabulation est l'annulation. Un dialogue destructeur dont le bouton dangereux
382
+ * est deja focalise se valide par une seule touche Entree — c'est-a-dire par le
383
+ * geste que la personne etait en train de faire quand il s'est ouvert.
384
+ */
385
+ class StConfirmDialog {
386
+ reference = inject(DialogRef);
387
+ libelles = inject(ST_LIBELLES);
388
+ demande = inject(DIALOG_DATA);
389
+ saisie = signal('', /* @ts-ignore */
390
+ ...(ngDevMode ? [{ debugName: "saisie" }] : /* istanbul ignore next */ []));
391
+ /** Unique par instance : deux confirmations empilees dupliqueraient un identifiant fixe. */
392
+ identifiantDeSaisie = `st-confirmation-saisie-${(compteur$3 += 1).toString()}`;
393
+ ton = computed(() => this.demande.destructeur === true ? 'danger' : 'primaire', /* @ts-ignore */
394
+ ...(ngDevMode ? [{ debugName: "ton" }] : /* istanbul ignore next */ []));
395
+ libelleConfirmation = computed(() => this.demande.libelleConfirmation ?? this.libelles().confirmer, /* @ts-ignore */
396
+ ...(ngDevMode ? [{ debugName: "libelleConfirmation" }] : /* istanbul ignore next */ []));
397
+ libelleAnnulation = computed(() => this.demande.libelleAnnulation ?? this.libelles().annuler, /* @ts-ignore */
398
+ ...(ngDevMode ? [{ debugName: "libelleAnnulation" }] : /* istanbul ignore next */ []));
399
+ libelleRecopie = computed(() => this.libelles().recopierPourConfirmer(this.demande.saisieDeConfirmation ?? ''), /* @ts-ignore */
400
+ ...(ngDevMode ? [{ debugName: "libelleRecopie" }] : /* istanbul ignore next */ []));
401
+ /**
402
+ * ⚠️ La comparaison ignore la casse et les espaces de bord, mais RIEN
403
+ * d'autre. Exiger la casse exacte transforme une confirmation en jeu
404
+ * d'adresse, et pousse a copier-coller — ce qui annule precisement l'effet
405
+ * recherche.
406
+ */
407
+ confirmationPossible = computed(() => {
408
+ const attendu = this.demande.saisieDeConfirmation;
409
+ if (attendu === undefined)
410
+ return true;
411
+ return this.saisie().trim().toLocaleLowerCase() === attendu.trim().toLocaleLowerCase();
412
+ }, /* @ts-ignore */
413
+ ...(ngDevMode ? [{ debugName: "confirmationPossible" }] : /* istanbul ignore next */ []));
414
+ auChangement(evenement) {
415
+ this.saisie.set(evenement.target.value);
416
+ }
417
+ confirmer() {
418
+ if (!this.confirmationPossible())
419
+ return;
420
+ this.reference.close(true);
421
+ }
422
+ annuler() {
423
+ this.reference.close(false);
424
+ }
425
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StConfirmDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
426
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StConfirmDialog, isStandalone: true, selector: "st-confirm-dialog", host: { classAttribute: "st-confirm-dialog" }, ngImport: i0, template: "<st-dialog-title>{{ demande.titre }}</st-dialog-title>\n\n<st-dialog-content>\n <p class=\"st-confirm-dialog__message\">{{ demande.message }}</p>\n\n @if (demande.saisieDeConfirmation !== undefined) {\n <div class=\"st-confirm-dialog__garde\">\n <label class=\"st-confirm-dialog__etiquette\" [attr.for]=\"identifiantDeSaisie\">\n {{ libelleRecopie() }}\n </label>\n <input\n [id]=\"identifiantDeSaisie\"\n class=\"st-confirm-dialog__champ\"\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [value]=\"saisie()\"\n (input)=\"auChangement($event)\"\n />\n </div>\n }\n</st-dialog-content>\n\n<st-dialog-actions>\n <button stButton variante=\"secondaire\" type=\"button\" (click)=\"annuler()\">\n {{ libelleAnnulation() }}\n </button>\n <button\n stButton\n type=\"button\"\n [variante]=\"ton() === 'danger' ? 'danger' : 'primaire'\"\n [indisponible]=\"!confirmationPossible()\"\n (click)=\"confirmer()\"\n >\n {{ libelleConfirmation() }}\n </button>\n</st-dialog-actions>\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-confirm-dialog__message{margin:0}.st-confirm-dialog__garde{display:flex;flex-direction:column;gap:var(--st-espacement-2);margin-block-start:var(--st-espacement-4)}.st-confirm-dialog__etiquette{font-size:var(--st-taille-sm);font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte)}.st-confirm-dialog__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-confirm-dialog__champ{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);border-radius:var(--st-rayon-sm)}\n"], dependencies: [{ kind: "component", type: StButton, selector: "button[stButton]", inputs: ["variante", "taille", "pleineLargeur", "iconeAvant", "iconeApres", "chargement", "indisponible"] }, { kind: "component", type: StDialogTitle, selector: "st-dialog-title" }, { kind: "component", type: StDialogContent, selector: "st-dialog-content", inputs: ["rembourrage"] }, { kind: "component", type: StDialogActions, selector: "st-dialog-actions", inputs: ["alignement"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
427
+ }
428
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StConfirmDialog, decorators: [{
429
+ type: Component,
430
+ args: [{ selector: 'st-confirm-dialog', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StButton, StDialogTitle, StDialogContent, StDialogActions], host: { class: 'st-confirm-dialog' }, template: "<st-dialog-title>{{ demande.titre }}</st-dialog-title>\n\n<st-dialog-content>\n <p class=\"st-confirm-dialog__message\">{{ demande.message }}</p>\n\n @if (demande.saisieDeConfirmation !== undefined) {\n <div class=\"st-confirm-dialog__garde\">\n <label class=\"st-confirm-dialog__etiquette\" [attr.for]=\"identifiantDeSaisie\">\n {{ libelleRecopie() }}\n </label>\n <input\n [id]=\"identifiantDeSaisie\"\n class=\"st-confirm-dialog__champ\"\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [value]=\"saisie()\"\n (input)=\"auChangement($event)\"\n />\n </div>\n }\n</st-dialog-content>\n\n<st-dialog-actions>\n <button stButton variante=\"secondaire\" type=\"button\" (click)=\"annuler()\">\n {{ libelleAnnulation() }}\n </button>\n <button\n stButton\n type=\"button\"\n [variante]=\"ton() === 'danger' ? 'danger' : 'primaire'\"\n [indisponible]=\"!confirmationPossible()\"\n (click)=\"confirmer()\"\n >\n {{ libelleConfirmation() }}\n </button>\n</st-dialog-actions>\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-confirm-dialog__message{margin:0}.st-confirm-dialog__garde{display:flex;flex-direction:column;gap:var(--st-espacement-2);margin-block-start:var(--st-espacement-4)}.st-confirm-dialog__etiquette{font-size:var(--st-taille-sm);font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte)}.st-confirm-dialog__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-confirm-dialog__champ{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);border-radius:var(--st-rayon-sm)}\n"] }]
431
+ }] });
432
+
433
+ let compteur$2 = 0;
434
+ /**
435
+ * Ouvre des dialogues modaux.
436
+ *
437
+ * ⚠️ Monte sur `@angular/cdk/dialog`, et c'est un choix defendu : le piegeage
438
+ * du focus, sa restauration au declencheur, `aria-modal`, la mise en retrait du
439
+ * reste de la page, Echap et le blocage du defilement sont longs a ecrire,
440
+ * faciles a croire finis, et c'est exactement la que l'accessibilite se perd.
441
+ * Le socle n'en reecrit aucune part.
442
+ *
443
+ * Ce qu'il ajoute au CDK : le focus sur le dialogue, `aria-modal`, un fond inerte
444
+ * qui laisse Echap fermer, et la fermeture a toute navigation du routeur.
445
+ */
446
+ class StDialog {
447
+ cdk = inject(Dialog);
448
+ overlay = inject(Overlay);
449
+ /** Une fonction de fermeture par dialogue ouvert : c'est tout ce qu'il faut garder. */
450
+ ouverts = new Set();
451
+ constructor() {
452
+ // ⚠️ `closeOnNavigation` du CDK ne couvre que l'HISTORIQUE du navigateur, sa
453
+ // documentation le dit : ni un lien du routeur, ni une navigation programmee.
454
+ // Un lien pose dans un tiroir de filtres changeait l'ecran dessous et
455
+ // laissait le tiroir pose dessus. Le routeur est optionnel : sans lui, il n'y
456
+ // a pas de navigation a suivre.
457
+ const routeur = inject(Router, { optional: true });
458
+ if (routeur === null)
459
+ return;
460
+ const abonnement = routeur.events.subscribe((evenement) => {
461
+ if (!(evenement instanceof NavigationStart))
462
+ return;
463
+ for (const fermer of [...this.ouverts])
464
+ fermer();
465
+ });
466
+ inject(DestroyRef).onDestroy(() => {
467
+ abonnement.unsubscribe();
468
+ });
469
+ }
470
+ /**
471
+ * Ouvre un composant, ou un GABARIT.
472
+ *
473
+ * Un gabarit sert quand le contenu vit deja dans le composant appelant — un
474
+ * tiroir de navigation qui deplace ses propres liens sur petit ecran. ⚠️ Son
475
+ * injecteur reste celui de l'endroit ou il est ECRIT : un `st-dialog-title`
476
+ * pose dedans ne recoit pas l'identifiant de titre, et le dialogue se nomme
477
+ * alors par `etiquette`.
478
+ */
479
+ ouvrir(composant, options = {}) {
480
+ compteur$2 += 1;
481
+ const identifiantDuTitre = `st-dialogue-titre-${compteur$2.toString()}`;
482
+ const fournisseurs = [
483
+ { provide: IDENTIFIANT_DU_TITRE, useValue: identifiantDuTitre },
484
+ ];
485
+ // L'argument de type porte le resultat jusqu'a l'appelant : l'omettre fait
486
+ // retomber l'inference sur `unknown`, et `fermer(resultat)` n'accepte plus
487
+ // rien de type.
488
+ const reference = this.cdk.open(composant, {
489
+ data: options.donnees,
490
+ providers: () => fournisseurs,
491
+ // ⚠️ Le conteneur du CDK n'a AUCUNE apparence : le dialogue s'affichait
492
+ // sans fond, le texte flottant sur la page. Voir `StDialogContainer`.
493
+ container: StDialogContainer,
494
+ ...this.geometrie(options),
495
+ panelClass: this.classes(options),
496
+ disableClose: options.fermetureInterdite === true || options.fondInerte === true,
497
+ // ⚠️ Nomme par son TITRE quand il en a un. Le dialogue sans nom accessible
498
+ // s'annonce « dialogue » et rien d'autre : la personne sait qu'une couche
499
+ // s'est ouverte, sans savoir laquelle.
500
+ ariaLabelledBy: identifiantDuTitre,
501
+ ariaLabel: options.etiquette ?? null,
502
+ ariaDescribedBy: options.descriptionDe ?? null,
503
+ // ⚠️ Pose EN PLUS du `aria-hidden` que le CDK met sur le reste de la page.
504
+ // Les deux signaux ne sont pas redondants : certaines technologies
505
+ // d'assistance ne lisent que l'un, et d'autres que l'autre.
506
+ ariaModal: true,
507
+ // ⚠️ Le focus va au DIALOGUE, non au premier element focalisable. Le
508
+ // defaut du CDK (`first-tabbable`) pose souvent le focus sur la croix de
509
+ // fermeture : le lecteur d'ecran annonce « Fermer », et rien de ce que le
510
+ // dialogue dit. Sur le conteneur, il lit le nom puis le contenu.
511
+ autoFocus: options.focusInitial ?? 'dialog',
512
+ role: options.role ?? 'dialog',
513
+ // ⚠️ Le focus retourne au declencheur a la fermeture. Sans cela, il
514
+ // retombe sur le corps du document et la navigation repart du haut.
515
+ restoreFocus: true,
516
+ // Une navigation arriere doit fermer la couche : sinon le dialogue reste
517
+ // pose sur un ecran qui a change dessous.
518
+ closeOnNavigation: true,
519
+ });
520
+ if (options.fondInerte === true && options.fermetureInterdite !== true) {
521
+ reference.keydownEvents.subscribe((evenement) => {
522
+ // `key` et non `keyCode`, deprecie — c'est pourtant `keyCode` que teste le
523
+ // CDK pour sa propre gestion d'Echap, desactivee ici par `disableClose`.
524
+ if (evenement.key !== 'Escape' || hasModifierKey(evenement))
525
+ return;
526
+ evenement.preventDefault();
527
+ reference.close();
528
+ });
529
+ }
530
+ const fermerSansResultat = () => {
531
+ reference.close();
532
+ };
533
+ this.ouverts.add(fermerSansResultat);
534
+ return {
535
+ ferme: new Promise((resoudre) => {
536
+ reference.closed.subscribe((resultat) => {
537
+ this.ouverts.delete(fermerSansResultat);
538
+ resoudre(resultat);
539
+ });
540
+ }),
541
+ fermer: (resultat) => {
542
+ reference.close(resultat);
543
+ },
544
+ };
545
+ }
546
+ /**
547
+ * ⚠️ Un tiroir occupe toute la hauteur du bord ou il s'ancre, et une feuille
548
+ * inferieure toute la largeur. Sans cela, le panneau flotte au milieu et ne
549
+ * se lit plus comme un tiroir.
550
+ */
551
+ geometrie(options) {
552
+ const cote = options.cote;
553
+ if (cote === undefined) {
554
+ return { width: options.largeur ?? 'min(32rem, calc(100vw - 2rem))' };
555
+ }
556
+ const position = this.overlay.position().global();
557
+ if (cote === 'bas') {
558
+ position.bottom('0').left('0');
559
+ return { width: '100%', positionStrategy: position };
560
+ }
561
+ // ⚠️ `start` et `end` du CDK, et non `left` et `right` : ils suivent le sens
562
+ // d'ecriture que le CDK connait deja. Une premiere version resolvait le
563
+ // sens a la main en lisant `dir` sur `<html>` — alors que le CDK le lit
564
+ // aussi sur `<body>`. Deux sources de verite pour la meme question finissent
565
+ // par diverger, et le tiroir s'ouvre alors du mauvais cote.
566
+ if (cote === 'fin')
567
+ position.end('0');
568
+ else
569
+ position.start('0');
570
+ position.top('0');
571
+ return {
572
+ width: options.largeur ?? 'min(28rem, calc(100vw - 3rem))',
573
+ height: '100%',
574
+ positionStrategy: position,
575
+ };
576
+ }
577
+ classes(options) {
578
+ const classes = ['st-dialogue'];
579
+ if (options.cote !== undefined)
580
+ classes.push('st-tiroir', `st-tiroir--${options.cote}`);
581
+ if (options.classe !== undefined)
582
+ classes.push(options.classe);
583
+ return classes;
584
+ }
585
+ /**
586
+ * Demande une confirmation, et rend `true` seulement si elle est donnee.
587
+ *
588
+ * ⚠️ La fermeture par Echap ou par le fond rend `false` : quitter n'est pas
589
+ * confirmer. C'est pourquoi la valeur est comparee explicitement plutot que
590
+ * testee pour sa veracite — `undefined` doit valoir non.
591
+ */
592
+ async confirmer(demande) {
593
+ const reference = this.ouvrir(StConfirmDialog, {
594
+ donnees: demande,
595
+ // ⚠️ Une action destructrice ne se ferme pas d'un clic a cote : le clic
596
+ // hors de la couche est le geste le plus facile a faire par megarde. Echap,
597
+ // lui, reste permis — il vaut annuler.
598
+ fondInerte: demande.destructeur === true,
599
+ });
600
+ return (await reference.ferme) === true;
601
+ }
602
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialog, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
603
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialog, providedIn: 'root' });
604
+ }
605
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialog, decorators: [{
606
+ type: Injectable,
607
+ args: [{ providedIn: 'root' }]
608
+ }], ctorParameters: () => [] });
609
+
610
+ /** Une FORME par ton, jamais la couleur seule. */
611
+ const ICONE = {
612
+ neutre: 'cercle-info',
613
+ primaire: 'cercle-info',
614
+ succes: 'cercle-coche',
615
+ avertissement: 'triangle-alerte',
616
+ danger: 'cercle-croix',
617
+ };
618
+ const DUREE_PAR_DEFAUT_MS = 5000;
619
+ /**
620
+ * ⚠️ Au-dela, les plus anciens messages SANS action sont retires. Une pile qui
621
+ * grandit sans limite finit par couvrir l'ecran, en poussant hors champ ce qui
622
+ * vient d'arriver.
623
+ */
624
+ const MAXIMUM_AFFICHE = 3;
625
+ /**
626
+ * Meme raison que dans `StAnnonceur` du noyau : la region doit etre observee
627
+ * avant de recevoir son texte, et un texte identique repose dans la meme tache
628
+ * ne produit aucune mutation.
629
+ */
630
+ const DELAI_D_ANNONCE_MS = 120;
631
+ /**
632
+ * Pile des messages ephemeres. Un seul exemplaire, pose par `StToast`.
633
+ *
634
+ * ⚠️ **Les messages visibles ne portent AUCUN role.** L'annonce passe par deux
635
+ * regions vivantes permanentes, creees vides avec le conteneur. La premiere
636
+ * version posait `role="status"` sur chaque message insere avec son texte :
637
+ * c'est la sequence que les lecteurs d'ecran n'annoncent pas de facon fiable,
638
+ * puisqu'ils rapportent les CHANGEMENTS d'une region deja observee. Les
639
+ * confirmations — le cas courant — risquaient le silence.
640
+ *
641
+ * ⚠️ Il ne prend JAMAIS le focus. Un message qui s'affiche pendant une saisie
642
+ * et vole le curseur fait perdre la frappe en cours.
643
+ */
644
+ class StToastContainer {
645
+ libelles = inject(ST_LIBELLES);
646
+ pile = signal([], /* @ts-ignore */
647
+ ...(ngDevMode ? [{ debugName: "pile" }] : /* istanbul ignore next */ []));
648
+ minuteries = new Map();
649
+ // Un minuteur PAR region : partage, une confirmation suivie d'une erreur dans
650
+ // la meme tache perdait la confirmation.
651
+ minuteriesDAnnonce = new Map();
652
+ compteur = 0;
653
+ messages = this.pile.asReadonly();
654
+ annoncePolie = signal('', /* @ts-ignore */
655
+ ...(ngDevMode ? [{ debugName: "annoncePolie" }] : /* istanbul ignore next */ []));
656
+ annonceAssertive = signal('', /* @ts-ignore */
657
+ ...(ngDevMode ? [{ debugName: "annonceAssertive" }] : /* istanbul ignore next */ []));
658
+ libelleFermeture = computed(() => this.libelles().fermerLeMessage, /* @ts-ignore */
659
+ ...(ngDevMode ? [{ debugName: "libelleFermeture" }] : /* istanbul ignore next */ []));
660
+ constructor() {
661
+ // Une micro-SPA federee se demonte : ses minuteries ne doivent pas lui
662
+ // survivre et agir sur un composant detruit.
663
+ inject(DestroyRef).onDestroy(() => {
664
+ for (const minuterie of this.minuteries.values())
665
+ clearTimeout(minuterie);
666
+ this.minuteries.clear();
667
+ for (const minuterie of this.minuteriesDAnnonce.values())
668
+ clearTimeout(minuterie);
669
+ });
670
+ }
671
+ ajouter(demande) {
672
+ this.compteur += 1;
673
+ const identifiant = this.compteur;
674
+ const toast = { ...demande, identifiant, ton: demande.ton ?? 'neutre' };
675
+ this.pile.update((existants) => this.borner([...existants, toast]));
676
+ this.programmer(toast);
677
+ this.annoncer(toast.message, toast.ton === 'danger' ? 'assertif' : 'poli');
678
+ return identifiant;
679
+ }
680
+ retirer(identifiant) {
681
+ this.arreterLaMinuterie(identifiant);
682
+ this.pile.update((existants) => existants.filter((toast) => toast.identifiant !== identifiant));
683
+ }
684
+ /**
685
+ * ⚠️ La minuterie s'arrete au survol ET au focus. Sans la seconde, une
686
+ * personne qui atteint le bouton au clavier le voit disparaitre sous son
687
+ * doigt ; sans la premiere, celle qui lit lentement n'a aucun moyen de
688
+ * retenir le message.
689
+ */
690
+ suspendre(identifiant) {
691
+ this.arreterLaMinuterie(identifiant);
692
+ }
693
+ reprendre(identifiant) {
694
+ const toast = this.pile().find((candidat) => candidat.identifiant === identifiant);
695
+ if (toast !== undefined)
696
+ this.programmer(toast);
697
+ }
698
+ agir(toast) {
699
+ toast.action?.faire();
700
+ this.retirer(toast.identifiant);
701
+ }
702
+ iconeDe(toast) {
703
+ return ICONE[toast.ton];
704
+ }
705
+ /**
706
+ * ⚠️ Un message porteur d'action n'est JAMAIS evince. La premiere version
707
+ * gardait les trois plus recents sans distinction : trois confirmations
708
+ * successives faisaient disparaitre un « Annuler » en silence — exactement ce
709
+ * que WCAG 2.2.1 interdit, et ce que le modele promettait d'eviter.
710
+ *
711
+ * Les plus anciens messages sans action partent d'abord. S'il ne reste que des
712
+ * messages porteurs d'action, la borne cede : mieux vaut une pile haute qu'une
713
+ * annulation perdue.
714
+ */
715
+ borner(pile) {
716
+ const resultat = [...pile];
717
+ while (resultat.length > MAXIMUM_AFFICHE) {
718
+ const indice = resultat.findIndex((toast) => toast.action === undefined);
719
+ if (indice === -1)
720
+ break;
721
+ const [evince] = resultat.splice(indice, 1);
722
+ if (evince !== undefined)
723
+ this.arreterLaMinuterie(evince.identifiant);
724
+ }
725
+ return resultat;
726
+ }
727
+ annoncer(texte, urgence) {
728
+ const region = urgence === 'assertif' ? this.annonceAssertive : this.annoncePolie;
729
+ const precedente = this.minuteriesDAnnonce.get(urgence);
730
+ if (precedente !== undefined)
731
+ clearTimeout(precedente);
732
+ region.set('');
733
+ this.minuteriesDAnnonce.set(urgence, setTimeout(() => {
734
+ region.set(texte);
735
+ this.minuteriesDAnnonce.delete(urgence);
736
+ }, DELAI_D_ANNONCE_MS));
737
+ }
738
+ /**
739
+ * Un message porteur d'action ne s'efface pas seul : voir `DemandeDeToast`,
740
+ * et WCAG 2.2.1.
741
+ */
742
+ programmer(toast) {
743
+ if (toast.action !== undefined)
744
+ return;
745
+ const duree = toast.dureeMs === undefined ? DUREE_PAR_DEFAUT_MS : toast.dureeMs;
746
+ if (duree === null)
747
+ return;
748
+ this.arreterLaMinuterie(toast.identifiant);
749
+ this.minuteries.set(toast.identifiant, setTimeout(() => {
750
+ this.retirer(toast.identifiant);
751
+ }, duree));
752
+ }
753
+ arreterLaMinuterie(identifiant) {
754
+ const minuterie = this.minuteries.get(identifiant);
755
+ if (minuterie !== undefined)
756
+ clearTimeout(minuterie);
757
+ this.minuteries.delete(identifiant);
758
+ }
759
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StToastContainer, deps: [], target: i0.ɵɵFactoryTarget.Component });
760
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StToastContainer, isStandalone: true, selector: "st-toast-container", host: { classAttribute: "st-toast-container" }, ngImport: i0, template: "<!-- Regions vivantes PERMANENTES, creees vides avec le conteneur : ce sont\n elles qui annoncent, jamais les messages visibles. -->\n<div class=\"st-toast-container__annonce\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {{ annoncePolie() }}\n</div>\n<div class=\"st-toast-container__annonce\" role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\">\n {{ annonceAssertive() }}\n</div>\n\n@for (toast of messages(); track toast.identifiant) {\n <div\n class=\"st-toast\"\n [attr.data-ton]=\"toast.ton\"\n (mouseenter)=\"suspendre(toast.identifiant)\"\n (mouseleave)=\"reprendre(toast.identifiant)\"\n (focusin)=\"suspendre(toast.identifiant)\"\n (focusout)=\"reprendre(toast.identifiant)\"\n >\n <st-icon [nom]=\"iconeDe(toast)\" taille=\"sm\" />\n <p class=\"st-toast__message\">{{ toast.message }}</p>\n\n @if (toast.action; as action) {\n <button stButton variante=\"discret\" taille=\"sm\" type=\"button\" (click)=\"agir(toast)\">\n {{ action.libelle }}\n </button>\n }\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 <button\n stIconButton\n type=\"button\"\n icone=\"croix\"\n taille=\"sm\"\n [etiquette]=\"libelleFermeture()\"\n (click)=\"retirer(toast.identifiant)\"\n ></button>\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{position:fixed;inset-block-end:var(--st-espacement-4);inset-inline:var(--st-espacement-4);z-index:var(--st-plan-notification);display:flex;flex-direction:column;gap:var(--st-espacement-2);align-items:center;pointer-events:none}.st-toast{display:flex;gap:var(--st-espacement-3);align-items:center;inline-size:100%;max-inline-size:28rem;padding:var(--st-espacement-3) var(--st-espacement-4);color:var(--st-couleur-texte);pointer-events:auto;background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-lg)}.st-toast__message{flex:1 1 auto;min-inline-size:0;margin:0}.st-toast[data-ton=succes] st-icon{color:var(--st-couleur-succes)}.st-toast[data-ton=avertissement] st-icon{color:var(--st-couleur-avertissement)}.st-toast[data-ton=danger] st-icon{color:var(--st-couleur-danger)}.st-toast[data-ton=primaire] st-icon{color:var(--st-couleur-primaire)}@media(forced-colors:active){.st-toast{border-color:CanvasText}}.st-toast-container__annonce{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}\n"], dependencies: [{ 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"] }, { kind: "component", type: StIconButton, selector: "button[stIconButton]", inputs: ["icone", "etiquette", "variante", "taille", "enfonce"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
761
+ }
762
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StToastContainer, decorators: [{
763
+ type: Component,
764
+ args: [{ selector: 'st-toast-container', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StIcon, StButton, StIconButton], host: { class: 'st-toast-container' }, template: "<!-- Regions vivantes PERMANENTES, creees vides avec le conteneur : ce sont\n elles qui annoncent, jamais les messages visibles. -->\n<div class=\"st-toast-container__annonce\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {{ annoncePolie() }}\n</div>\n<div class=\"st-toast-container__annonce\" role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\">\n {{ annonceAssertive() }}\n</div>\n\n@for (toast of messages(); track toast.identifiant) {\n <div\n class=\"st-toast\"\n [attr.data-ton]=\"toast.ton\"\n (mouseenter)=\"suspendre(toast.identifiant)\"\n (mouseleave)=\"reprendre(toast.identifiant)\"\n (focusin)=\"suspendre(toast.identifiant)\"\n (focusout)=\"reprendre(toast.identifiant)\"\n >\n <st-icon [nom]=\"iconeDe(toast)\" taille=\"sm\" />\n <p class=\"st-toast__message\">{{ toast.message }}</p>\n\n @if (toast.action; as action) {\n <button stButton variante=\"discret\" taille=\"sm\" type=\"button\" (click)=\"agir(toast)\">\n {{ action.libelle }}\n </button>\n }\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 <button\n stIconButton\n type=\"button\"\n icone=\"croix\"\n taille=\"sm\"\n [etiquette]=\"libelleFermeture()\"\n (click)=\"retirer(toast.identifiant)\"\n ></button>\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{position:fixed;inset-block-end:var(--st-espacement-4);inset-inline:var(--st-espacement-4);z-index:var(--st-plan-notification);display:flex;flex-direction:column;gap:var(--st-espacement-2);align-items:center;pointer-events:none}.st-toast{display:flex;gap:var(--st-espacement-3);align-items:center;inline-size:100%;max-inline-size:28rem;padding:var(--st-espacement-3) var(--st-espacement-4);color:var(--st-couleur-texte);pointer-events:auto;background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-lg)}.st-toast__message{flex:1 1 auto;min-inline-size:0;margin:0}.st-toast[data-ton=succes] st-icon{color:var(--st-couleur-succes)}.st-toast[data-ton=avertissement] st-icon{color:var(--st-couleur-avertissement)}.st-toast[data-ton=danger] st-icon{color:var(--st-couleur-danger)}.st-toast[data-ton=primaire] st-icon{color:var(--st-couleur-primaire)}@media(forced-colors:active){.st-toast{border-color:CanvasText}}.st-toast-container__annonce{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}\n"] }]
765
+ }], ctorParameters: () => [] });
766
+
767
+ /**
768
+ * Messages ephemeres.
769
+ *
770
+ * ```ts
771
+ * const messages = inject(StToast);
772
+ * messages.montrer({ message: 'Virement enregistre', ton: 'succes' });
773
+ * messages.montrer({
774
+ * message: 'Beneficiaire supprime',
775
+ * action: { libelle: 'Annuler', faire: () => this.retablir() },
776
+ * });
777
+ * ```
778
+ *
779
+ * ⚠️ **Un seul conteneur pour toute l'application**, cree a la premiere
780
+ * demande. Deux piles superposees se recouvrent, et la seconde masque la
781
+ * premiere sans que rien ne le signale.
782
+ *
783
+ * ⚠️ **A n'employer que pour ce qui peut etre manque.** Un message ephemere
784
+ * n'est pas une facon d'annoncer une erreur : si l'information compte, elle
785
+ * doit rester a l'ecran — `st-banner` pour la page, `st-inline-message` pour un
786
+ * champ. La regle de tri : si la personne qui n'a pas vu le message est
787
+ * penalisee, ce n'est pas un toast.
788
+ */
789
+ class StToast {
790
+ overlay = inject(Overlay);
791
+ couche = null;
792
+ conteneur = null;
793
+ constructor() {
794
+ // La couche est liberee avec le service : une micro-SPA federee se demonte,
795
+ // et une couche orpheline resterait posee dans le document de l'hote.
796
+ inject(DestroyRef).onDestroy(() => {
797
+ this.couche?.dispose();
798
+ this.couche = null;
799
+ this.conteneur = null;
800
+ });
801
+ }
802
+ /** Affiche un message. Rend son identifiant, pour pouvoir le retirer. */
803
+ montrer(demande) {
804
+ return this.pile().ajouter(demande);
805
+ }
806
+ retirer(identifiant) {
807
+ this.conteneur?.instance.retirer(identifiant);
808
+ }
809
+ pile() {
810
+ if (this.conteneur !== null)
811
+ return this.conteneur.instance;
812
+ // La couche d'overlay du CDK, et non le corps du document : elle est deja
813
+ // au bon plan, et le socle n'a pas a se disputer le `z-index` avec la page.
814
+ this.couche = this.overlay.create({ panelClass: 'st-toast-couche' });
815
+ this.conteneur = this.couche.attach(new ComponentPortal(StToastContainer));
816
+ return this.conteneur.instance;
817
+ }
818
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StToast, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
819
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StToast, providedIn: 'root' });
820
+ }
821
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StToast, decorators: [{
822
+ type: Injectable,
823
+ args: [{ providedIn: 'root' }]
824
+ }], ctorParameters: () => [] });
825
+
826
+ /**
827
+ * Seuils ou le temps restant s'annonce.
828
+ *
829
+ * ⚠️ JAMAIS a chaque seconde : une region qui change toutes les secondes
830
+ * recouvre tout ce que le lecteur d'ecran dit, y compris les deux boutons que la
831
+ * personne cherche justement a atteindre.
832
+ */
833
+ const SEUILS_D_ANNONCE = [60, 30, 10];
834
+ /** Contenu du dialogue d'avertissement. Ouvert par `st-idle-warning`, jamais directement. */
835
+ class StIdleWarningPanel {
836
+ reference = inject(DialogRef);
837
+ libelles = inject(ST_LIBELLES);
838
+ locale = inject(ST_LOCALE);
839
+ donnees = inject(DIALOG_DATA);
840
+ annonce = new AnnonceLocale(inject(DestroyRef));
841
+ titre = computed(() => this.libelles().sessionBientotExpiree, /* @ts-ignore */
842
+ ...(ngDevMode ? [{ debugName: "titre" }] : /* istanbul ignore next */ []));
843
+ message = computed(() => this.libelles().sessionExpireDans(formaterDuree(this.donnees.restant(), this.locale())), /* @ts-ignore */
844
+ ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
845
+ libelleRester = computed(() => this.libelles().resterConnecte, /* @ts-ignore */
846
+ ...(ngDevMode ? [{ debugName: "libelleRester" }] : /* istanbul ignore next */ []));
847
+ libelleDeconnecter = computed(() => this.libelles().seDeconnecter, /* @ts-ignore */
848
+ ...(ngDevMode ? [{ debugName: "libelleDeconnecter" }] : /* istanbul ignore next */ []));
849
+ constructor() {
850
+ // A l'ouverture, le dialogue d'alerte est lu avec sa description : rien a
851
+ // annoncer de plus. Ensuite, seul le franchissement d'un seuil s'annonce.
852
+ let precedent = null;
853
+ effect(() => {
854
+ const restant = this.donnees.restant();
855
+ const franchi = precedent !== null &&
856
+ SEUILS_D_ANNONCE.some((seuil) => (precedent ?? 0) > seuil && restant <= seuil);
857
+ precedent = restant;
858
+ if (franchi)
859
+ this.annonce.annoncer(this.message());
860
+ });
861
+ }
862
+ rester() {
863
+ this.reference.close('prolonger');
864
+ }
865
+ deconnecter() {
866
+ this.reference.close('deconnecter');
867
+ }
868
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StIdleWarningPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
869
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: StIdleWarningPanel, isStandalone: true, selector: "st-idle-warning-panel", host: { classAttribute: "st-idle-warning-panel" }, ngImport: i0, template: "<st-dialog-title>{{ titre() }}</st-dialog-title>\n\n<st-dialog-content>\n <p class=\"st-idle-warning-panel__message\" [id]=\"donnees.identifiantDeDescription\">\n {{ message() }}\n </p>\n <span class=\"st-vh\" aria-live=\"polite\">{{ annonce.texte() }}</span>\n</st-dialog-content>\n\n<!-- L'action destructrice a gauche, la moins destructrice a droite et en\n primaire : c'est elle qui recoit le focus a l'ouverture. -->\n<st-dialog-actions>\n <button stButton variante=\"secondaire\" type=\"button\" (click)=\"deconnecter()\">\n {{ libelleDeconnecter() }}\n </button>\n <button\n class=\"st-idle-warning-panel__rester\"\n stButton\n variante=\"primaire\"\n type=\"button\"\n (click)=\"rester()\"\n >\n {{ libelleRester() }}\n </button>\n</st-dialog-actions>\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-idle-warning-panel__message{margin:0;font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "component", type: StButton, selector: "button[stButton]", inputs: ["variante", "taille", "pleineLargeur", "iconeAvant", "iconeApres", "chargement", "indisponible"] }, { kind: "component", type: StDialogTitle, selector: "st-dialog-title" }, { kind: "component", type: StDialogContent, selector: "st-dialog-content", inputs: ["rembourrage"] }, { kind: "component", type: StDialogActions, selector: "st-dialog-actions", inputs: ["alignement"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
870
+ }
871
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StIdleWarningPanel, decorators: [{
872
+ type: Component,
873
+ args: [{ selector: 'st-idle-warning-panel', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StButton, StDialogTitle, StDialogContent, StDialogActions], host: { class: 'st-idle-warning-panel' }, template: "<st-dialog-title>{{ titre() }}</st-dialog-title>\n\n<st-dialog-content>\n <p class=\"st-idle-warning-panel__message\" [id]=\"donnees.identifiantDeDescription\">\n {{ message() }}\n </p>\n <span class=\"st-vh\" aria-live=\"polite\">{{ annonce.texte() }}</span>\n</st-dialog-content>\n\n<!-- L'action destructrice a gauche, la moins destructrice a droite et en\n primaire : c'est elle qui recoit le focus a l'ouverture. -->\n<st-dialog-actions>\n <button stButton variante=\"secondaire\" type=\"button\" (click)=\"deconnecter()\">\n {{ libelleDeconnecter() }}\n </button>\n <button\n class=\"st-idle-warning-panel__rester\"\n stButton\n variante=\"primaire\"\n type=\"button\"\n (click)=\"rester()\"\n >\n {{ libelleRester() }}\n </button>\n</st-dialog-actions>\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-idle-warning-panel__message{margin:0;font-variant-numeric:tabular-nums}\n"] }]
874
+ }], ctorParameters: () => [] });
875
+
876
+ let compteur$1 = 0;
877
+ /**
878
+ * Avertissement avant fermeture de session pour inactivite.
879
+ *
880
+ * ```html
881
+ * <st-idle-warning
882
+ * [secondesRestantes]="inactivite.secondesRestantes()"
883
+ * (prolonge)="inactivite.prolonger()"
884
+ * (deconnecte)="session.deconnecter()"
885
+ * />
886
+ * ```
887
+ *
888
+ * ⚠️ **Composant de PRESENTATION.** Il n'injecte pas `StInactivite` : `ui` ne
889
+ * depend pas de `@starterlib/core` (ADR 0003). Il s'ouvre quand
890
+ * `secondesRestantes` cesse d'etre `null`, et se ferme quand il y revient. La
891
+ * decision de fermer la session reste au noyau, qui la prend cote serveur.
892
+ *
893
+ * ⚠️ **Un `alertdialog`, focus sur « Rester connecte ».** Le motif ARIA veut le
894
+ * focus sur l'action la moins destructrice : une touche Entree reflexe doit
895
+ * garder la session, jamais la fermer.
896
+ *
897
+ * ⚠️ **Le clic a cote ne prolonge rien**, pour la raison que donne `StInactivite`
898
+ * du noyau : le dialogue disparaitrait sans que la session ait ete prolongee cote
899
+ * serveur. Il faut repondre. Echap, geste delibere, vaut « Rester connecte ».
900
+ */
901
+ class StIdleWarning {
902
+ dialogues = inject(StDialog);
903
+ /** `null` : aucun avertissement en cours. */
904
+ secondesRestantes = input(null, /* @ts-ignore */
905
+ ...(ngDevMode ? [{ debugName: "secondesRestantes" }] : /* istanbul ignore next */ []));
906
+ prolonge = output();
907
+ deconnecte = output();
908
+ restant = signal(0, /* @ts-ignore */
909
+ ...(ngDevMode ? [{ debugName: "restant" }] : /* istanbul ignore next */ []));
910
+ reference = null;
911
+ identifiantDeDescription = `st-inactivite-${(compteur$1 += 1).toString()}`;
912
+ constructor() {
913
+ effect(() => {
914
+ const secondes = this.secondesRestantes();
915
+ if (secondes === null) {
916
+ this.fermerSansReponse();
917
+ return;
918
+ }
919
+ this.restant.set(secondes);
920
+ if (this.reference === null)
921
+ this.ouvrir();
922
+ });
923
+ inject(DestroyRef).onDestroy(() => {
924
+ this.fermerSansReponse();
925
+ });
926
+ }
927
+ ouvrir() {
928
+ const donnees = {
929
+ restant: this.restant.asReadonly(),
930
+ identifiantDeDescription: this.identifiantDeDescription,
931
+ };
932
+ const reference = this.dialogues.ouvrir(StIdleWarningPanel, {
933
+ donnees,
934
+ role: 'alertdialog',
935
+ descriptionDe: this.identifiantDeDescription,
936
+ focusInitial: '.st-idle-warning-panel__rester',
937
+ fondInerte: true,
938
+ });
939
+ this.reference = reference;
940
+ void reference.ferme.then((reponse) => {
941
+ // Fermeture decidee ici — fin du decompte, composant detruit : pas de reponse.
942
+ if (this.reference !== reference)
943
+ return;
944
+ this.reference = null;
945
+ if (reponse === 'deconnecter')
946
+ this.deconnecte.emit();
947
+ // Echap, ou « Rester connecte » : la personne est la.
948
+ else
949
+ this.prolonge.emit();
950
+ });
951
+ }
952
+ fermerSansReponse() {
953
+ const reference = this.reference;
954
+ this.reference = null;
955
+ reference?.fermer();
956
+ }
957
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StIdleWarning, deps: [], target: i0.ɵɵFactoryTarget.Component });
958
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StIdleWarning, isStandalone: true, selector: "st-idle-warning", inputs: { secondesRestantes: { classPropertyName: "secondesRestantes", publicName: "secondesRestantes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { prolonge: "prolonge", deconnecte: "deconnecte" }, host: { classAttribute: "st-idle-warning" }, ngImport: i0, template: "", changeDetection: i0.ChangeDetectionStrategy.OnPush });
959
+ }
960
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StIdleWarning, decorators: [{
961
+ type: Component,
962
+ args: [{ selector: 'st-idle-warning', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'st-idle-warning' }, template: "" }]
963
+ }], ctorParameters: () => [], propDecorators: { secondesRestantes: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondesRestantes", required: false }] }], prolonge: [{ type: i0.Output, args: ["prolonge"] }], deconnecte: [{ type: i0.Output, args: ["deconnecte"] }] } });
964
+
965
+ /**
966
+ * Bulle visible d'une infobulle. Posee par `stTooltip`, jamais directement.
967
+ *
968
+ * ⚠️ Muette (`aria-hidden`) : le texte est deja lu comme DESCRIPTION du
969
+ * declencheur, par un noeud persistant. L'exposer aussi ici le ferait lire deux
970
+ * fois.
971
+ */
972
+ class StTooltipPanel {
973
+ texte = input.required(/* @ts-ignore */
974
+ ...(ngDevMode ? [{ debugName: "texte" }] : /* istanbul ignore next */ []));
975
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StTooltipPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
976
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StTooltipPanel, isStandalone: true, selector: "st-tooltip-panel", inputs: { texte: { classPropertyName: "texte", publicName: "texte", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, classAttribute: "st-tooltip-panel" }, ngImport: i0, template: "{{ texte() }}\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;max-inline-size:18rem;padding:var(--st-espacement-1) var(--st-espacement-2);font-size:var(--st-taille-sm);line-height:var(--st-interligne-normal);color:var(--st-couleur-texte-inverse);pointer-events:auto;background:var(--st-couleur-texte);border-radius:var(--st-rayon-sm);box-shadow:var(--st-ombre-md)}@media(forced-colors:active){:host{border:1px solid CanvasText}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
977
+ }
978
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StTooltipPanel, decorators: [{
979
+ type: Component,
980
+ args: [{ selector: 'st-tooltip-panel', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'st-tooltip-panel', 'aria-hidden': 'true' }, template: "{{ texte() }}\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;max-inline-size:18rem;padding:var(--st-espacement-1) var(--st-espacement-2);font-size:var(--st-taille-sm);line-height:var(--st-interligne-normal);color:var(--st-couleur-texte-inverse);pointer-events:auto;background:var(--st-couleur-texte);border-radius:var(--st-rayon-sm);box-shadow:var(--st-ombre-md)}@media(forced-colors:active){:host{border:1px solid CanvasText}}\n"] }]
981
+ }], propDecorators: { texte: [{ type: i0.Input, args: [{ isSignal: true, alias: "texte", required: true }] }] } });
982
+
983
+ const POSITIONS$1 = {
984
+ haut: [
985
+ { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -6 },
986
+ { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: 6 },
987
+ ],
988
+ bas: [
989
+ { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: 6 },
990
+ { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -6 },
991
+ ],
992
+ };
993
+ /** Delai au survol : sans lui, balayer une barre d'outils fait clignoter chaque bulle. */
994
+ const DELAI_AU_SURVOL_MS = 300;
995
+ /**
996
+ * Delai avant de masquer quand le pointeur quitte le declencheur : le temps de
997
+ * le poser sur la bulle, qui doit pouvoir etre survolee (WCAG 1.4.13).
998
+ */
999
+ const DELAI_DE_GRACE_MS = 120;
1000
+ /**
1001
+ * Infobulle : un complement de texte, au survol ET au focus.
1002
+ *
1003
+ * ```html
1004
+ * <button stIconButton icone="corbeille" etiquette="Supprimer" stTooltip="Supprimer le beneficiaire"></button>
1005
+ * ```
1006
+ *
1007
+ * WCAG 1.4.13, les trois exigences, chacune tenue ici :
1008
+ *
1009
+ * - **Refermable** : Echap la masque SANS deplacer le focus, et sans fermer le
1010
+ * dialogue qui la contiendrait — la premiere touche est pour elle.
1011
+ * - **Survolable** : le pointeur peut passer du declencheur a la bulle sans
1012
+ * qu'elle disparaisse.
1013
+ * - **Persistante** : elle reste tant que le survol ou le focus dure.
1014
+ *
1015
+ * ⚠️ **Le texte est une DESCRIPTION posee des le depart**, par un noeud
1016
+ * persistant. Un `aria-describedby` qui viserait la bulle elle-meme pointerait
1017
+ * vers un element absent au moment ou le focus arrive : le lecteur d'ecran,
1018
+ * qui calcule la description a cet instant, ne lirait rien.
1019
+ *
1020
+ * ⚠️ **Jamais d'information exclusive, jamais de contenu interactif.** Sur un
1021
+ * ecran tactile, il n'y a ni survol ni focus visible : l'infobulle n'existe pas.
1022
+ * Ce qu'elle dit doit pouvoir manquer. Un lien ou un bouton dedans serait
1023
+ * inatteignable — c'est un `st-popover` qu'il faut alors.
1024
+ */
1025
+ class StTooltip {
1026
+ element = inject(ElementRef).nativeElement;
1027
+ overlay = inject(Overlay);
1028
+ descripteur = inject(AriaDescriber);
1029
+ document = inject(DOCUMENT);
1030
+ stTooltip = input.required(/* @ts-ignore */
1031
+ ...(ngDevMode ? [{ debugName: "stTooltip" }] : /* istanbul ignore next */ []));
1032
+ positionDInfobulle = input('haut', /* @ts-ignore */
1033
+ ...(ngDevMode ? [{ debugName: "positionDInfobulle" }] : /* istanbul ignore next */ []));
1034
+ couche = null;
1035
+ minuterie = null;
1036
+ description = null;
1037
+ echap = (evenement) => {
1038
+ if (evenement.key !== 'Escape' || this.couche === null)
1039
+ return;
1040
+ // Echap est pour la bulle d'abord : sans cet arret, il fermerait aussi le
1041
+ // dialogue qui la contient, et la personne perdrait bien plus qu'une bulle.
1042
+ evenement.stopPropagation();
1043
+ this.masquer();
1044
+ };
1045
+ constructor() {
1046
+ effect(() => {
1047
+ const texte = this.stTooltip().trim();
1048
+ this.retirerLaDescription();
1049
+ if (texte === '')
1050
+ return;
1051
+ this.descripteur.describe(this.element, texte);
1052
+ this.description = texte;
1053
+ });
1054
+ inject(DestroyRef).onDestroy(() => {
1055
+ this.masquer();
1056
+ this.retirerLaDescription();
1057
+ });
1058
+ }
1059
+ auSurvol() {
1060
+ this.programmer(() => {
1061
+ this.afficher();
1062
+ }, DELAI_AU_SURVOL_MS);
1063
+ }
1064
+ auDepartDuPointeur() {
1065
+ this.programmer(() => {
1066
+ this.masquer();
1067
+ }, DELAI_DE_GRACE_MS);
1068
+ }
1069
+ afficher() {
1070
+ this.annulerLaMinuterie();
1071
+ const texte = this.stTooltip().trim();
1072
+ if (texte === '' || this.couche !== null)
1073
+ return;
1074
+ this.couche = this.overlay.create({
1075
+ positionStrategy: this.overlay
1076
+ .position()
1077
+ .flexibleConnectedTo(this.element)
1078
+ .withPositions(POSITIONS$1[this.positionDInfobulle()])
1079
+ .withPush(false),
1080
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
1081
+ panelClass: 'st-tooltip-couche',
1082
+ });
1083
+ const bulle = this.couche.attach(new ComponentPortal(StTooltipPanel));
1084
+ bulle.setInput('texte', texte);
1085
+ const noeud = bulle.location.nativeElement;
1086
+ noeud.addEventListener('mouseenter', () => {
1087
+ this.annulerLaMinuterie();
1088
+ });
1089
+ noeud.addEventListener('mouseleave', () => {
1090
+ this.auDepartDuPointeur();
1091
+ });
1092
+ // En capture : l'infobulle entend Echap avant le dialogue qui la contient.
1093
+ this.document.addEventListener('keydown', this.echap, true);
1094
+ }
1095
+ masquer() {
1096
+ this.annulerLaMinuterie();
1097
+ if (this.couche === null)
1098
+ return;
1099
+ this.document.removeEventListener('keydown', this.echap, true);
1100
+ this.couche.dispose();
1101
+ this.couche = null;
1102
+ }
1103
+ programmer(action, delai) {
1104
+ this.annulerLaMinuterie();
1105
+ this.minuterie = setTimeout(action, delai);
1106
+ }
1107
+ annulerLaMinuterie() {
1108
+ if (this.minuterie !== null)
1109
+ clearTimeout(this.minuterie);
1110
+ this.minuterie = null;
1111
+ }
1112
+ retirerLaDescription() {
1113
+ if (this.description === null)
1114
+ return;
1115
+ this.descripteur.removeDescription(this.element, this.description);
1116
+ this.description = null;
1117
+ }
1118
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StTooltip, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1119
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: StTooltip, isStandalone: true, selector: "[stTooltip]", inputs: { stTooltip: { classPropertyName: "stTooltip", publicName: "stTooltip", isSignal: true, isRequired: true, transformFunction: null }, positionDInfobulle: { classPropertyName: "positionDInfobulle", publicName: "positionDInfobulle", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "auSurvol()", "mouseleave": "auDepartDuPointeur()", "focusin": "afficher()", "focusout": "masquer()" } }, ngImport: i0 });
1120
+ }
1121
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StTooltip, decorators: [{
1122
+ type: Directive,
1123
+ args: [{
1124
+ selector: '[stTooltip]',
1125
+ host: {
1126
+ '(mouseenter)': 'auSurvol()',
1127
+ '(mouseleave)': 'auDepartDuPointeur()',
1128
+ '(focusin)': 'afficher()',
1129
+ '(focusout)': 'masquer()',
1130
+ },
1131
+ }]
1132
+ }], ctorParameters: () => [], propDecorators: { stTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "stTooltip", required: true }] }], positionDInfobulle: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionDInfobulle", required: false }] }] } });
1133
+
1134
+ const POSITIONS = {
1135
+ debut: [
1136
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 6 },
1137
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -6 },
1138
+ ],
1139
+ fin: [
1140
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 6 },
1141
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -6 },
1142
+ ],
1143
+ };
1144
+ const FOCALISABLES = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), ' +
1145
+ 'textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
1146
+ let compteur = 0;
1147
+ /**
1148
+ * Couche NON modale attachee a un declencheur : filtres rapides, detail d'une
1149
+ * ligne, petit formulaire.
1150
+ *
1151
+ * ```html
1152
+ * <button stButton [stPopoverTrigger]="filtres">Filtres</button>
1153
+ * <st-popover #filtres etiquette="Filtres des operations">…</st-popover>
1154
+ * ```
1155
+ *
1156
+ * Distinct de l'infobulle, qui ne contient que du texte, et du dialogue, qui
1157
+ * piege le focus et met la page en retrait.
1158
+ *
1159
+ * ⚠️ **Non modale, donc aucun piege.** La tabulation peut en sortir, et la page
1160
+ * reste utilisable. Mais une couche que le focus a quittee flotterait sans lien
1161
+ * avec ce qu'on fait : elle se ferme quand le focus part ailleurs que sur elle
1162
+ * ou son declencheur.
1163
+ *
1164
+ * ⚠️ **Deux fermetures, deux destinations du focus.** Echap rend le focus au
1165
+ * declencheur : la personne n'a rien choisi d'autre. Un clic a l'exterieur le
1166
+ * laisse la ou il a ete pose : le lui reprendre annulerait ce clic.
1167
+ *
1168
+ * Meme structure que `st-menu` : le declencheur DESIGNE la couche, et l'element
1169
+ * est DEPLACE dans la couche d'overlay a l'ouverture, avec ses liaisons et son
1170
+ * etat.
1171
+ */
1172
+ class StPopover {
1173
+ element = inject(ElementRef);
1174
+ overlay = inject(Overlay);
1175
+ /** Nom accessible : sans lui, la couche s'annonce « dialogue » et rien d'autre. */
1176
+ etiquette = input.required(/* @ts-ignore */
1177
+ ...(ngDevMode ? [{ debugName: "etiquette" }] : /* istanbul ignore next */ []));
1178
+ alignement = input('debut', /* @ts-ignore */
1179
+ ...(ngDevMode ? [{ debugName: "alignement" }] : /* istanbul ignore next */ []));
1180
+ identifiant = `st-popover-${(compteur += 1).toString()}`;
1181
+ ouvertInterne = signal(false, /* @ts-ignore */
1182
+ ...(ngDevMode ? [{ debugName: "ouvertInterne" }] : /* istanbul ignore next */ []));
1183
+ ouvert = this.ouvertInterne.asReadonly();
1184
+ couche = null;
1185
+ declencheur = null;
1186
+ constructor() {
1187
+ inject(DestroyRef).onDestroy(() => {
1188
+ this.detruireLaCouche();
1189
+ });
1190
+ }
1191
+ basculerDepuis(declencheur) {
1192
+ if (this.ouvertInterne())
1193
+ this.fermer();
1194
+ else
1195
+ this.ouvrirDepuis(declencheur);
1196
+ }
1197
+ ouvrirDepuis(declencheur) {
1198
+ if (this.ouvertInterne())
1199
+ return;
1200
+ this.declencheur = declencheur;
1201
+ this.couche = this.overlay.create({
1202
+ positionStrategy: this.overlay
1203
+ .position()
1204
+ .flexibleConnectedTo(declencheur)
1205
+ .withPositions(POSITIONS[this.alignement()])
1206
+ .withPush(true),
1207
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
1208
+ });
1209
+ this.couche.attach(new DomPortal(this.element));
1210
+ this.ouvertInterne.set(true);
1211
+ this.couche.outsidePointerEvents().subscribe((evenement) => {
1212
+ // Le clic sur le declencheur est deja une bascule : le traiter aussi ici
1213
+ // rouvrirait la couche aussitot fermee.
1214
+ if (declencheur.contains(evenement.target))
1215
+ return;
1216
+ this.fermer({ rendreLeFocus: false });
1217
+ });
1218
+ // Le focus entre dans la couche : sur son premier controle s'il en a, sinon
1219
+ // sur la couche elle-meme, pour que son nom soit lu.
1220
+ const hote = this.element.nativeElement;
1221
+ const premier = hote.querySelector(FOCALISABLES);
1222
+ (premier ?? hote).focus();
1223
+ }
1224
+ fermer(options = {}) {
1225
+ if (!this.ouvertInterne())
1226
+ return;
1227
+ this.ouvertInterne.set(false);
1228
+ this.detruireLaCouche();
1229
+ const declencheur = this.declencheur;
1230
+ this.declencheur = null;
1231
+ if (options.rendreLeFocus !== false)
1232
+ declencheur?.focus();
1233
+ }
1234
+ auClavier(evenement) {
1235
+ if (evenement.key !== 'Escape')
1236
+ return;
1237
+ evenement.preventDefault();
1238
+ evenement.stopPropagation();
1239
+ this.fermer();
1240
+ }
1241
+ /**
1242
+ * ⚠️ L'element actif est lu APRES la transition de focus, et non dans
1243
+ * `relatedTarget`. Ce dernier est vide quand le focus part vers un element
1244
+ * sans focus, et n'est pas renseigne partout ; pendant la transition elle-meme,
1245
+ * l'element actif est encore le corps du document.
1246
+ */
1247
+ aLaPerteDuFocus() {
1248
+ setTimeout(() => {
1249
+ if (!this.ouvertInterne())
1250
+ return;
1251
+ const actif = this.element.nativeElement.ownerDocument.activeElement;
1252
+ // Le corps du document : le focus a quitte la page, ou un clic sans cible
1253
+ // focalisable — deja traite comme clic a l'exterieur par la couche.
1254
+ if (actif === null || actif === actif.ownerDocument.body)
1255
+ return;
1256
+ if (this.element.nativeElement.contains(actif))
1257
+ return;
1258
+ if (this.declencheur?.contains(actif) === true)
1259
+ return;
1260
+ this.fermer({ rendreLeFocus: false });
1261
+ });
1262
+ }
1263
+ detruireLaCouche() {
1264
+ this.couche?.dispose();
1265
+ this.couche = null;
1266
+ }
1267
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPopover, deps: [], target: i0.ɵɵFactoryTarget.Component });
1268
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StPopover, isStandalone: true, selector: "st-popover", inputs: { etiquette: { classPropertyName: "etiquette", publicName: "etiquette", isSignal: true, isRequired: true, transformFunction: null }, alignement: { classPropertyName: "alignement", publicName: "alignement", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "dialog", "tabindex": "-1" }, listeners: { "keydown": "auClavier($event)", "focusout": "aLaPerteDuFocus()" }, properties: { "id": "identifiant", "attr.aria-label": "etiquette()", "attr.data-ouvert": "ouvert() ? \"\" : null" }, classAttribute: "st-popover" }, ngImport: i0, template: "<ng-content />\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:none}:host([data-ouvert]){display:block;max-inline-size:min(24rem,100vw - 2rem);padding:var(--st-espacement-4);color:var(--st-couleur-texte);background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-lg)}:host(:focus){outline:none}@media(forced-colors:active){:host([data-ouvert]){border-color:CanvasText}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1269
+ }
1270
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPopover, decorators: [{
1271
+ type: Component,
1272
+ args: [{ selector: 'st-popover', changeDetection: ChangeDetectionStrategy.OnPush, host: {
1273
+ class: 'st-popover',
1274
+ role: 'dialog',
1275
+ tabindex: '-1',
1276
+ '[id]': 'identifiant',
1277
+ '[attr.aria-label]': 'etiquette()',
1278
+ '[attr.data-ouvert]': 'ouvert() ? "" : null',
1279
+ '(keydown)': 'auClavier($event)',
1280
+ '(focusout)': 'aLaPerteDuFocus()',
1281
+ }, template: "<ng-content />\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:none}:host([data-ouvert]){display:block;max-inline-size:min(24rem,100vw - 2rem);padding:var(--st-espacement-4);color:var(--st-couleur-texte);background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-lg)}:host(:focus){outline:none}@media(forced-colors:active){:host([data-ouvert]){border-color:CanvasText}}\n"] }]
1282
+ }], ctorParameters: () => [], propDecorators: { etiquette: [{ type: i0.Input, args: [{ isSignal: true, alias: "etiquette", required: true }] }], alignement: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignement", required: false }] }] } });
1283
+
1284
+ /**
1285
+ * Declencheur d'un `st-popover`.
1286
+ *
1287
+ * ⚠️ `aria-controls` vise la couche meme fermee : le lien entre les deux existe
1288
+ * pour les technologies d'assistance avant l'ouverture, et `aria-expanded` dit
1289
+ * son etat.
1290
+ */
1291
+ class StPopoverTrigger {
1292
+ element = inject(ElementRef).nativeElement;
1293
+ stPopoverTrigger = input.required(/* @ts-ignore */
1294
+ ...(ngDevMode ? [{ debugName: "stPopoverTrigger" }] : /* istanbul ignore next */ []));
1295
+ basculer() {
1296
+ this.stPopoverTrigger().basculerDepuis(this.element);
1297
+ }
1298
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPopoverTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1299
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: StPopoverTrigger, isStandalone: true, selector: "[stPopoverTrigger]", inputs: { stPopoverTrigger: { classPropertyName: "stPopoverTrigger", publicName: "stPopoverTrigger", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "dialog" }, listeners: { "click": "basculer()" }, properties: { "attr.aria-expanded": "stPopoverTrigger().ouvert()", "attr.aria-controls": "stPopoverTrigger().identifiant" } }, ngImport: i0 });
1300
+ }
1301
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPopoverTrigger, decorators: [{
1302
+ type: Directive,
1303
+ args: [{
1304
+ selector: '[stPopoverTrigger]',
1305
+ host: {
1306
+ 'aria-haspopup': 'dialog',
1307
+ '[attr.aria-expanded]': 'stPopoverTrigger().ouvert()',
1308
+ '[attr.aria-controls]': 'stPopoverTrigger().identifiant',
1309
+ '(click)': 'basculer()',
1310
+ },
1311
+ }]
1312
+ }], propDecorators: { stPopoverTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "stPopoverTrigger", required: true }] }] } });
1313
+
222
1314
  /**
223
1315
  * @starterlib/ui/surfaces — ce qui se pose AU-DESSUS de la page.
224
1316
  *
@@ -231,8 +1323,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
231
1323
  * decoupage par famille multiplierait les points d'entree a tenir sans rien
232
1324
  * retirer de plus au chargement.
233
1325
  *
1326
+ * ⚠️ En federation, ce point d'entree se partage SEPAREMENT de
1327
+ * `@starterlib/ui` : c'est un specificateur de module distinct.
1328
+ *
234
1329
  * ```ts
235
- * import { StMenu, StMenuItem, StMenuTrigger } from '@starterlib/ui/surfaces';
1330
+ * import { StMenu, StDialog } from '@starterlib/ui/surfaces';
236
1331
  * ```
237
1332
  */
238
1333
 
@@ -240,5 +1335,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
240
1335
  * Generated bundle index. Do not edit.
241
1336
  */
242
1337
 
243
- export { StMenu, StMenuItem, StMenuTrigger };
1338
+ export { IDENTIFIANT_DU_TITRE, StConfirmDialog, StDialog, StDialogActions, StDialogContainer, StDialogContent, StDialogTitle, StIdleWarning, StIdleWarningPanel, StMenu, StMenuItem, StMenuTrigger, StPopover, StPopoverTrigger, StToast, StToastContainer, StTooltip, StTooltipPanel };
244
1339
  //# sourceMappingURL=starterlib-ui-surfaces.mjs.map