@ktortu/aaa 0.9.7 → 0.9.9

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/dialog/dialog.css CHANGED
@@ -54,8 +54,16 @@
54
54
  }
55
55
 
56
56
  /* ============ Dimensionnement du pane overlay (panelClass: kt-dialog) ============ */
57
+ /* Le PLAFOND réel vient de la config (`KT_DIALOG_AAA_DEFAULTS.maxWidth = var(--dialog-max-width)`),
58
+ posé en STYLE INLINE sur le pane par l'overlay CDK. Indispensable : le CDK injecte à
59
+ l'exécution `.cdk-overlay-pane { max-width: 100% }` HORS layer, qui bat toute règle de cette
60
+ feuille quand elle est importée via layer(kt-aaa). La règle ci-dessous n'est qu'une
61
+ CEINTURE-BRETELLES pour un import SANS layer (spécificité 0,2,0 > 0,1,0).
62
+ Mode centré (défaut) : la classe .kt-dialog--centered ne porte VOLONTAIREMENT aucune règle
63
+ interne — hook public, symétrique de --fullscreen/--sheet, qui permet aux devs de
64
+ cibler/identifier le mode centré en CSS. Ne pas la supprimer. */
57
65
  .cdk-overlay-pane.kt-dialog {
58
- inline-size: var(--dialog-max-width);
66
+ max-inline-size: var(--dialog-max-width);
59
67
  }
60
68
 
61
69
  /* ============ Surface = conteneur CDK (porte role="dialog") ============ */
@@ -63,6 +71,7 @@
63
71
  display: flex;
64
72
  flex-direction: column;
65
73
  box-sizing: border-box;
74
+ inline-size: 100%;
66
75
  /* Reflow AAA (1.4.10) : conteneur plafonné en hauteur ; le contenu long ([ktDialogContent])
67
76
  scrolle verticalement (jamais en 2D), titre et actions restant visibles. */
68
77
  max-block-size: var(--dialog-max-height);
@@ -255,6 +264,9 @@
255
264
  /* ===================== PLEIN ÉCRAN (kt-dialog--fullscreen) =====================
256
265
  Surface bord-à-bord ; [ktDialogHeader] devient la top app bar. */
257
266
  .cdk-overlay-pane.kt-dialog--fullscreen {
267
+ /* Retune le token lu par le style inline du pane (config `maxWidth`) : bord-à-bord. */
268
+ --dialog-max-width: 100vw;
269
+
258
270
  inline-size: 100vw;
259
271
  max-inline-size: 100vw;
260
272
  }
@@ -288,53 +300,97 @@
288
300
  padding-block-end: max(var(--dialog-pad), env(safe-area-inset-bottom, 0px));
289
301
  }
290
302
 
291
- /* ===================== BOTTOM-SHEET (kt-dialog--sheet) =====================
292
- Feuille glissant du bas, tokens --kt-sheet-* (partagés avec le Select). On positionne+translate
293
- le CONTENEUR ; la directive [ktDialogSheetHandle] le drag via .cdk-dialog-container. */
303
+ /* ===================== BOTTOM-SHEET (kt-dialog--sheet) — ADR-0005 =====================
304
+ Le CONTENEUR est un scroller plein écran à snap : le spacer (un écran) porte le snap « fermé »,
305
+ la carte (.kt-dialog-container__layout) le snap « ouvert ». Le drag-to-dismiss s'attrape
306
+ PARTOUT au toucher (scroll natif, arbitrage par latching avec [ktDialogContent]) ;
307
+ entrée/sortie = scrolls programmatiques (KtDialogContainer). Tokens --kt-sheet-* partagés. */
308
+ .cdk-overlay-pane.kt-dialog--sheet {
309
+ /* Retune le token lu par le style inline du pane (config `maxWidth`) : pleine largeur.
310
+ (Le conteneur est en position:fixed — seul le token du pane doit suivre.) */
311
+ --dialog-max-width: 100%;
312
+ }
294
313
  .cdk-overlay-pane.kt-dialog--sheet .cdk-dialog-container {
295
314
  position: fixed;
296
- inset: auto 0 0 0;
315
+ inset: 0;
316
+ display: block;
297
317
  inline-size: 100%;
298
318
  max-inline-size: 100%;
319
+ max-block-size: none;
320
+ overflow-y: auto;
321
+ scroll-snap-type: y mandatory;
322
+ /* Pas de scroll chaining vers la page (l'overlay CDK bloque déjà — ceinture et bretelles). */
323
+ overscroll-behavior: contain;
324
+ /* Le scroller EST le geste : pas de barre de défilement visible sur l'overlay. */
325
+ scrollbar-width: none;
326
+ background: transparent;
327
+ border: 0;
328
+ border-radius: 0;
329
+ box-shadow: none;
330
+ backdrop-filter: none;
331
+ transform: none;
332
+ animation: none;
333
+ transition: none;
334
+ /* Le scroller DOIT capter le tactile (`pointer-events: auto`, hérité du pane). Un
335
+ `pointer-events: none` (pour laisser les taps traverser vers le backdrop CDK) EMPÊCHE iOS
336
+ Safari de défiler ce conteneur au doigt : le geste de drag-to-dismiss fuit alors vers la
337
+ page (pull-to-refresh) et la sheet ne se ferme jamais. Même choix que le popup du Select.
338
+ Le scrim sombre reste le backdrop CDK (derrière ce conteneur transparent) ; la fermeture
339
+ au tap-extérieur est portée par le spacer (`(click)` → `onScrimClick`), le verrou de scroll
340
+ du fond par `KtBodyScrollLock` (cf. `KtDialogContainer`). */
341
+ }
342
+
343
+ /* Spacer de dismiss : un écran entier au-dessus de la carte (snap « fermé »). Cliquable pour
344
+ fermer (tap-extérieur, façon backdrop) ET saisissable au doigt pour le drag (scroll natif). */
345
+ .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__spacer {
346
+ block-size: 100%;
347
+ scroll-snap-align: start;
348
+ scroll-snap-stop: always;
349
+ cursor: default;
350
+ }
351
+
352
+ /* La carte : reprend la surface du dialog (déplacée du conteneur, devenu scroller). */
353
+ .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__layout {
354
+ scroll-snap-align: end;
355
+ scroll-snap-stop: always;
356
+ display: flex;
357
+ flex-direction: column;
358
+ box-sizing: border-box;
359
+ inline-size: 100%;
299
360
  max-block-size: var(--kt-sheet-max-block-size, 85svh);
361
+ overflow: hidden;
362
+ color: var(--dialog-fg);
363
+ background: var(--dialog-bg);
364
+ border: var(--dialog-border-width) solid var(--dialog-border-color);
300
365
  border-block-end: 0;
301
366
  border-start-start-radius: var(--kt-sheet-radius, 16px);
302
367
  border-start-end-radius: var(--kt-sheet-radius, 16px);
303
368
  border-end-start-radius: 0;
304
369
  border-end-end-radius: 0;
305
370
  box-shadow: var(--kt-sheet-shadow, 0 -4px 16px rgb(0 0 0 / 12%));
306
- transform: none;
307
- translate: 0 0;
308
- /* Entrée : glissement du bas (keyframe PARTAGÉE kt-sheet-in, cf. foundation.css). */
309
- animation: kt-sheet-in var(--kt-sheet-anim-duration, 120ms) ease;
310
- transition: translate var(--kt-sheet-anim-duration, 120ms) ease;
311
- }
312
- .cdk-overlay-pane.kt-dialog--sheet .cdk-dialog-container.kt-dialog-sheet--dragging,
313
- .cdk-overlay-pane.kt-dialog--sheet .cdk-dialog-container.kt-dialog-container--dragging {
314
- transition: none;
371
+ backdrop-filter: var(--dialog-backdrop-filter, none);
315
372
  }
316
- .cdk-overlay-pane.kt-dialog--sheet .cdk-dialog-container.kt-dialog-container--closing {
317
- translate: 0 100%;
318
- transition: translate var(--kt-sheet-exit-duration, 90ms) cubic-bezier(0.4, 0, 0.2, 1);
373
+
374
+ /* Relais flex vers l'hôte projeté : la carte remplace le conteneur comme colonne
375
+ (header/actions fixes, [ktDialogContent] seul scrollable). */
376
+ .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__layout > * {
377
+ display: flex;
378
+ flex-direction: column;
379
+ flex: 1 1 auto;
380
+ min-block-size: 0;
319
381
  }
320
382
 
321
- /* Poignée de préhension ([ktDialogSheetHandle] ou .kt-dialog-container__sheet-handle) visible dès que le dialog est en mode sheet.
322
- Hors mode sheet, l'élément reste un div vide de hauteur nulle (aucune règle ne s'applique). */
323
- .cdk-overlay-pane.kt-dialog--sheet [ktDialogSheetHandle],
383
+ /* Poignée DÉCORATIVE auto-rendue par le conteneur (ADR-0005) : simple affordance « ceci se
384
+ tire » — la sheet s'attrape partout. Ni cursor grab (mensonger : pas de drag souris), ni
385
+ touch-action (le geste EST le scroll), ni cible 44px (ce n'est plus un contrôle).
386
+ Opt-out : panelClass additionnel `kt-dialog--no-handle` à l'ouverture. */
324
387
  .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__sheet-handle {
325
388
  display: flex;
326
389
  align-items: center;
327
390
  justify-content: center;
328
391
  flex: none;
329
- block-size: 44px; /* Cible tactile minimale WCAG 2.5.5 AAA (44px) */
330
- cursor: grab;
331
- touch-action: none; /* le geste ne déclenche pas le scroll de la page */
332
- }
333
- .cdk-overlay-pane.kt-dialog--sheet [ktDialogSheetHandle]:active,
334
- .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__sheet-handle:active {
335
- cursor: grabbing;
392
+ block-size: 28px;
336
393
  }
337
- .cdk-overlay-pane.kt-dialog--sheet [ktDialogSheetHandle]::before,
338
394
  .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__sheet-handle::before {
339
395
  content: '';
340
396
  inline-size: 2.25rem;
@@ -343,9 +399,8 @@
343
399
  background: var(--kt-sheet-grab-color, var(--kt-outline, #c4c7c5));
344
400
  }
345
401
 
346
- /* Mouvement réduit : pas de glissement ni de fondu (WCAG 2.3.3). */
402
+ /* Mouvement réduit : pas de fondu (WCAG 2.3.3). La sheet gère le sien en JS (scroll instantané). */
347
403
  @media (prefers-reduced-motion: reduce) {
348
- .cdk-overlay-pane.kt-dialog--sheet .cdk-dialog-container,
349
404
  .cdk-overlay-pane.kt-dialog--fullscreen .cdk-dialog-container {
350
405
  transition: none;
351
406
  translate: 0 0;
@@ -14,6 +14,11 @@ import { map } from 'rxjs';
14
14
  * gère que le glissement. Le gating « écran compact uniquement » reste à l'appelant (signal `isCompact`
15
15
  * de `KtViewport`) — la primitive est volontairement agnostique.
16
16
  *
17
+ * @deprecated ADR-0005 — les sheets de la lib (Select compact, Dialog `sheet`) sont désormais des
18
+ * scrollers à snap : le drag-to-dismiss « attrapable partout » et l'arbitrage avec le scroll
19
+ * interne sont NATIFS ; cette primitive « poignée + translate » n'a plus de consommateur interne.
20
+ * Sera supprimée dans une prochaine version majeure.
21
+ *
17
22
  * @param opts Élément à translater, callback de fermeture, classe de drag et seuil optionnel.
18
23
  * @returns Un contrôleur {@link KtSheetDrag} (`start` à brancher sur `pointerdown`, `destroy` au teardown).
19
24
  * @example
@@ -1 +1 @@
1
- {"version":3,"file":"ktortu-aaa-cdk.mjs","sources":["../../../../projects/ktortu/aaa/cdk/sheet/sheet-drag.ts","../../../../projects/ktortu/aaa/cdk/sheet/body-scroll-lock.ts","../../../../projects/ktortu/aaa/cdk/breakpoints/breakpoints.ts","../../../../projects/ktortu/aaa/cdk/breakpoints/viewport.ts","../../../../projects/ktortu/aaa/cdk/id-generator.ts","../../../../projects/ktortu/aaa/cdk/audit.ts","../../../../projects/ktortu/aaa/cdk/ktortu-aaa-cdk.ts"],"sourcesContent":["/** Contrôleur de drag-to-dismiss d'une bottom-sheet. À câbler sur le `pointerdown` de la poignée. */\nexport interface KtSheetDrag {\n /** Démarre le suivi du glissement ; à brancher sur le `pointerdown` de la poignée. */\n start(event: PointerEvent): void;\n /** Détache les écouteurs et réinitialise l'état ; à appeler à la destruction. */\n destroy(): void;\n}\n\nexport interface KtSheetDragOptions {\n /** Élément translaté pendant le glissement (la feuille elle-même). Lu à chaque `start`. */\n pane: () => HTMLElement | null;\n /** Fermeture déclenchée quand le seuil est franchi (ex. `expanded.set(false)` / `dialogRef.close()`). */\n onDismiss: () => void;\n /** Classe togglée sur le pane pendant le drag (coupe la transition CSS du glissement). */\n draggingClass: string;\n /** Fraction de la hauteur de la feuille au-delà de laquelle on ferme (défaut 0.25). */\n threshold?: number;\n}\n\n/**\n * Drag-to-dismiss vertical (vers le BAS uniquement) d'une bottom-sheet, factorisé entre le Select\n * (popup Popover) et le Dialog (pane CDK). Chaque appelant ne fournit que SON élément à translater\n * et SON callback de fermeture.\n *\n * Geste DOUBLÉ côté appelant par Échap + bouton Fermer + tap-extérieur (WCAG 2.5.1) : ce module ne\n * gère que le glissement. Le gating « écran compact uniquement » reste à l'appelant (signal `isCompact`\n * de `KtViewport`) — la primitive est volontairement agnostique.\n *\n * @param opts Élément à translater, callback de fermeture, classe de drag et seuil optionnel.\n * @returns Un contrôleur {@link KtSheetDrag} (`start` à brancher sur `pointerdown`, `destroy` au teardown).\n * @example\n * ```ts\n * const drag = createKtSheetDrag({\n * pane: () => paneEl(),\n * onDismiss: () => dialogRef.close(),\n * draggingClass: 'kt-sheet--dragging',\n * });\n * // <div class=\"handle\" (pointerdown)=\"drag.start($event)\"></div>\n * ```\n */\nexport function createKtSheetDrag(opts: KtSheetDragOptions): KtSheetDrag {\n const threshold = opts.threshold ?? 0.25;\n let startY = 0;\n let pane: HTMLElement | null = null;\n let handle: HTMLElement | null = null;\n let ownerDoc: Document | null = null;\n\n const cleanupListeners = (): void => {\n if (!ownerDoc) return;\n ownerDoc.removeEventListener('pointermove', onMove);\n ownerDoc.removeEventListener('pointerup', onEnd);\n ownerDoc.removeEventListener('pointercancel', onEnd);\n };\n\n const onMove = (event: PointerEvent): void => {\n if (!pane) return;\n const dy = Math.max(0, event.clientY - startY); // on ne tire que vers le bas\n pane.style.translate = `0 ${dy}px`;\n };\n\n const onEnd = (event: PointerEvent): void => {\n if (!pane || !handle) return;\n cleanupListeners();\n\n const dragged = event.clientY - startY;\n const dismiss = dragged > pane.getBoundingClientRect().height * threshold;\n // WCAG 2.3.3 : sous prefers-reduced-motion, on applique l'état final SANS animation.\n const reduceMotion = !!ownerDoc?.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n\n if (reduceMotion) {\n // draggingClass (transition coupée) encore présente : la translation finale est instantanée,\n // puis on retire la classe (aucun changement de valeur ⇒ aucune animation déclenchée).\n pane.style.translate = dismiss ? '0 100%' : '';\n pane.classList.remove(opts.draggingClass);\n } else {\n pane.classList.remove(opts.draggingClass); // réactive la transition CSS (glissement / snap-back)\n pane.style.translate = dismiss ? '0 100%' : ''; // dismiss : glisse jusqu'en bas ; sinon snap-back à 0\n }\n if (dismiss) opts.onDismiss();\n\n pane = null;\n handle = null;\n ownerDoc = null;\n };\n\n return {\n start(event: PointerEvent): void {\n if (event.button !== 0) return;\n const el = opts.pane();\n if (!el) return;\n handle = event.currentTarget as HTMLElement;\n ownerDoc = handle.ownerDocument;\n // La poignée ne doit PAS voler le focus (sinon un close-on-blur fermerait prématurément).\n event.preventDefault();\n startY = event.clientY;\n pane = el;\n pane.classList.add(opts.draggingClass);\n // Écoute au niveau du document : un pointer qui sort de la poignée (petite cible) ou une\n // capture perdue n'interrompt plus le suivi du glissement ni le snap-back.\n ownerDoc.addEventListener('pointermove', onMove);\n ownerDoc.addEventListener('pointerup', onEnd);\n ownerDoc.addEventListener('pointercancel', onEnd);\n },\n destroy(): void {\n cleanupListeners();\n pane = null;\n handle = null;\n ownerDoc = null;\n },\n };\n}\n","import { DOCUMENT } from '@angular/common';\nimport { Injectable, inject } from '@angular/core';\n\n/**\n * Verrou de défilement du `<body>` à COMPTEUR DE RÉFÉRENCES, partagé (`providedIn: 'root'`).\n *\n * Plusieurs surfaces modales simultanées (sheets, popups) peuvent verrouiller le scroll du fond :\n * `overflow: hidden` n'est posé qu'au premier `lock()` et restauré (à sa valeur d'origine) qu'au\n * dernier `unlock()`. Sans ce compteur, deux consommateurs par-instance se désynchronisent — la\n * fermeture du premier restaure le scroll alors que le second est encore ouvert.\n */\n@Injectable({ providedIn: 'root' })\nexport class KtBodyScrollLock {\n private readonly doc = inject(DOCUMENT);\n private count = 0;\n private previousOverflow = '';\n\n /** Pose un verrou (premier appelant : mémorise puis force `overflow: hidden`). */\n lock(): void {\n if (this.count === 0) {\n this.previousOverflow = this.doc.body.style.overflow;\n this.doc.body.style.overflow = 'hidden';\n }\n this.count++;\n }\n\n /** Relâche un verrou ; restaure l'`overflow` d'origine quand le dernier consommateur relâche. */\n unlock(): void {\n if (this.count === 0) return;\n this.count--;\n if (this.count === 0) {\n this.doc.body.style.overflow = this.previousOverflow;\n }\n }\n}\n","import { InjectionToken, Provider } from '@angular/core';\n\n/**\n * Valeur d'un palier de breakpoint, CONFIGURABLE PAR APPLICATION :\n * - un **nombre** = largeur (px) à partir de laquelle le palier commence ; la lib en dérive une\n * media query LARGEUR SEULE (bornes calculées par rapport au palier suivant) ;\n * - une **chaîne** = media query complète du palier, utilisée VERBATIM (échappatoire pour un critère\n * non-largeur : `orientation`, `pointer`, ratio…).\n */\nexport type KtBreakpointValue = number | string;\n\n/**\n * Seuils responsive de la lib, surchargeables par appli via `provideKtBreakpoints`. Sémantique\n * « plancher de palier » : chaque valeur indique où le palier COMMENCE → 3 bandes mutuellement\n * exclusives (`mobile` < `tablet` < `desktop`).\n */\nexport interface KtBreakpoints {\n /** Plancher du mobile (typiquement 0). */\n mobile: KtBreakpointValue;\n /** Largeur où commence la tablette (= plafond mobile). */\n tablet: KtBreakpointValue;\n /** Largeur où commence le desktop (= plafond tablette). */\n desktop: KtBreakpointValue;\n}\n\n/** Défauts : bascule bottom-sheet sous 600px (aligné sur l'ancien `MOBILE_QUERY`). */\nexport const KT_DEFAULT_BREAKPOINTS: KtBreakpoints = { mobile: 0, tablet: 600, desktop: 1024 };\n\n/** Token des seuils responsive. Défaut = `KT_DEFAULT_BREAKPOINTS` ; surchargé par `provideKtBreakpoints`. */\nexport const KT_BREAKPOINTS = new InjectionToken<KtBreakpoints>('KT_BREAKPOINTS', {\n providedIn: 'root',\n factory: () => KT_DEFAULT_BREAKPOINTS,\n});\n\n/**\n * À ajouter aux `providers` de `app.config.ts`. Fusionne avec les défauts → surcharge partielle OK.\n * @param overrides Seuils à surcharger ; les clés absentes gardent leur défaut `KT_DEFAULT_BREAKPOINTS`.\n * @returns Provider liant `KT_BREAKPOINTS` aux seuils fusionnés.\n * @example provideKtBreakpoints({ tablet: 768, desktop: 1200 })\n * @example provideKtBreakpoints({ tablet: '(min-width: 768px) and (orientation: landscape)' })\n */\nexport function provideKtBreakpoints(overrides: Partial<KtBreakpoints>): Provider {\n return { provide: KT_BREAKPOINTS, useValue: { ...KT_DEFAULT_BREAKPOINTS, ...overrides } };\n}\n\n/** Trois chaînes de media queries résolues, prêtes pour `matchMedia` / `BreakpointObserver`. */\nexport interface KtBreakpointMedia {\n /** Media query de la bande mobile. */\n mobile: string;\n /** Media query de la bande tablette. */\n tablet: string;\n /** Media query de la bande desktop. */\n desktop: string;\n}\n\n/** Évite le chevauchement d'1px entre deux bandes adjacentes (max-width juste sous le plancher suivant). */\nconst EPSILON = 0.02;\n\nfunction minWidth(v: number): string {\n return `(min-width: ${v}px)`;\n}\n\nfunction maxWidth(nextFloor: number): string {\n return `(max-width: ${nextFloor - EPSILON}px)`;\n}\n\n/** `not all` ne matche jamais : repli sûr (= desktop) si une bande numérique se retrouve sans borne. */\nfunction join(...parts: string[]): string {\n return parts.filter(Boolean).join(' and ') || 'not all';\n}\n\n/**\n * Convertit les seuils (nombres et/ou chaînes) en 3 media queries. Un palier-chaîne est repris\n * verbatim ; un palier-nombre devient la bande `[plancher courant .. plancher suivant[`. Si le\n * plancher suivant est une chaîne (pas de borne numérique), la bande reste ouverte de ce côté.\n * @param bp Seuils responsive à convertir (nombres et/ou chaînes).\n * @returns Les trois media queries résolues (`mobile`, `tablet`, `desktop`).\n * @example resolveKtBreakpointMedia(KT_DEFAULT_BREAKPOINTS)\n */\nexport function resolveKtBreakpointMedia(bp: KtBreakpoints): KtBreakpointMedia {\n const { mobile, tablet, desktop } = bp;\n\n return {\n mobile:\n typeof mobile === 'string'\n ? mobile\n : join(mobile > 0 ? minWidth(mobile) : '', typeof tablet === 'number' ? maxWidth(tablet) : ''),\n tablet:\n typeof tablet === 'string'\n ? tablet\n : join(minWidth(tablet), typeof desktop === 'number' ? maxWidth(desktop) : ''),\n desktop: typeof desktop === 'string' ? desktop : minWidth(desktop),\n };\n}\n","import { Injectable, Signal, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { BreakpointObserver } from '@angular/cdk/layout';\nimport { map } from 'rxjs';\nimport { KT_BREAKPOINTS, resolveKtBreakpointMedia } from './breakpoints';\n\n/**\n * Expose l'état responsive courant en SIGNALS, dérivé des seuils configurés par l'appli\n * (`KT_BREAKPOINTS` / `provideKtBreakpoints`). Fondé sur `BreakpointObserver` (`@angular/cdk/layout`,\n * déjà dépendance) : SSR-safe (`matches: false` côté serveur → desktop par défaut) et nettoyage des\n * listeners géré par le CDK.\n *\n * À injecter aussi bien dans les composants de la lib (bascule bottom-sheet) que dans le code des\n * applis consommatrices pour leur propre layout responsive.\n */\n@Injectable({ providedIn: 'root' })\nexport class KtViewport {\n private readonly observer = inject(BreakpointObserver);\n\n /** Media queries résolues (largeur dérivée ou verbatim selon le token) — utile pour miroir CSS. */\n readonly media = resolveKtBreakpointMedia(inject(KT_BREAKPOINTS));\n\n /** Le viewport est dans la bande mobile. */\n readonly isMobile = this.matches(this.media.mobile);\n /** Le viewport est dans la bande tablette. */\n readonly isTablet = this.matches(this.media.tablet);\n /** Le viewport est dans la bande desktop. */\n readonly isDesktop = this.matches(this.media.desktop);\n /** Alias de `isMobile` : palier où les composants de la lib basculent en bottom-sheet. */\n readonly isCompact = this.isMobile;\n\n private matches(query: string): Signal<boolean> {\n return toSignal(this.observer.observe(query).pipe(map((state) => state.matches)), {\n initialValue: false,\n });\n }\n}\n","import { Injectable } from '@angular/core';\n\n/**\n * Génère des identifiants numériques incrémentaux uniques au sein de l'application, soit globalement,\n * soit par préfixe (compteur dédié). Sert à construire des `anchor-name` / `id` stables et distincts.\n *\n * @example\n * ```ts\n * const idGen = inject(KtIdGenerator);\n * const anchor = `--kt-menu-anchor-${idGen.generateId('menu')}`;\n * ```\n */\n@Injectable({\n // Stryker disable next-line all: `providedIn` doit rester statiquement analysable par l'AOT (NG1010).\n providedIn: 'root',\n})\nexport class KtIdGenerator {\n private readonly counters = new Map<string, number>();\n private nextGlobalId = 0;\n\n /**\n * Renvoie le prochain identifiant. Sans `prefix`, utilise le compteur global ; avec `prefix`,\n * utilise un compteur dédié à ce préfixe (séquences indépendantes).\n * @param prefix Clé optionnelle de compteur ; chaque préfixe a sa propre séquence.\n * @returns Le compteur courant (number) pour la portée demandée, avant incrément.\n */\n generateId(prefix?: string): number {\n if (!prefix) {\n return this.nextGlobalId++;\n }\n const current = this.counters.get(prefix) ?? 0;\n this.counters.set(prefix, current + 1);\n return current;\n }\n}\n","import { InjectionToken, isDevMode } from '@angular/core';\n\n/**\n * InjectionToken permettant d'activer ou désactiver les audits d'accessibilité à l'exécution\n * (gardes-fous console.warn en mode dev). Activé par défaut si `isDevMode()` est vrai.\n */\nexport const KT_AUDIT_ENABLED = new InjectionToken<boolean>('KT_AUDIT_ENABLED', {\n providedIn: 'root',\n factory: () => isDevMode(),\n});\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ktortu-aaa-cdk.mjs","sources":["../../../../projects/ktortu/aaa/cdk/sheet/sheet-drag.ts","../../../../projects/ktortu/aaa/cdk/sheet/body-scroll-lock.ts","../../../../projects/ktortu/aaa/cdk/breakpoints/breakpoints.ts","../../../../projects/ktortu/aaa/cdk/breakpoints/viewport.ts","../../../../projects/ktortu/aaa/cdk/id-generator.ts","../../../../projects/ktortu/aaa/cdk/audit.ts","../../../../projects/ktortu/aaa/cdk/ktortu-aaa-cdk.ts"],"sourcesContent":["/** Contrôleur de drag-to-dismiss d'une bottom-sheet. À câbler sur le `pointerdown` de la poignée. */\nexport interface KtSheetDrag {\n /** Démarre le suivi du glissement ; à brancher sur le `pointerdown` de la poignée. */\n start(event: PointerEvent): void;\n /** Détache les écouteurs et réinitialise l'état ; à appeler à la destruction. */\n destroy(): void;\n}\n\nexport interface KtSheetDragOptions {\n /** Élément translaté pendant le glissement (la feuille elle-même). Lu à chaque `start`. */\n pane: () => HTMLElement | null;\n /** Fermeture déclenchée quand le seuil est franchi (ex. `expanded.set(false)` / `dialogRef.close()`). */\n onDismiss: () => void;\n /** Classe togglée sur le pane pendant le drag (coupe la transition CSS du glissement). */\n draggingClass: string;\n /** Fraction de la hauteur de la feuille au-delà de laquelle on ferme (défaut 0.25). */\n threshold?: number;\n}\n\n/**\n * Drag-to-dismiss vertical (vers le BAS uniquement) d'une bottom-sheet, factorisé entre le Select\n * (popup Popover) et le Dialog (pane CDK). Chaque appelant ne fournit que SON élément à translater\n * et SON callback de fermeture.\n *\n * Geste DOUBLÉ côté appelant par Échap + bouton Fermer + tap-extérieur (WCAG 2.5.1) : ce module ne\n * gère que le glissement. Le gating « écran compact uniquement » reste à l'appelant (signal `isCompact`\n * de `KtViewport`) — la primitive est volontairement agnostique.\n *\n * @deprecated ADR-0005 — les sheets de la lib (Select compact, Dialog `sheet`) sont désormais des\n * scrollers à snap : le drag-to-dismiss « attrapable partout » et l'arbitrage avec le scroll\n * interne sont NATIFS ; cette primitive « poignée + translate » n'a plus de consommateur interne.\n * Sera supprimée dans une prochaine version majeure.\n *\n * @param opts Élément à translater, callback de fermeture, classe de drag et seuil optionnel.\n * @returns Un contrôleur {@link KtSheetDrag} (`start` à brancher sur `pointerdown`, `destroy` au teardown).\n * @example\n * ```ts\n * const drag = createKtSheetDrag({\n * pane: () => paneEl(),\n * onDismiss: () => dialogRef.close(),\n * draggingClass: 'kt-sheet--dragging',\n * });\n * // <div class=\"handle\" (pointerdown)=\"drag.start($event)\"></div>\n * ```\n */\nexport function createKtSheetDrag(opts: KtSheetDragOptions): KtSheetDrag {\n const threshold = opts.threshold ?? 0.25;\n let startY = 0;\n let pane: HTMLElement | null = null;\n let handle: HTMLElement | null = null;\n let ownerDoc: Document | null = null;\n\n const cleanupListeners = (): void => {\n if (!ownerDoc) return;\n ownerDoc.removeEventListener('pointermove', onMove);\n ownerDoc.removeEventListener('pointerup', onEnd);\n ownerDoc.removeEventListener('pointercancel', onEnd);\n };\n\n const onMove = (event: PointerEvent): void => {\n if (!pane) return;\n const dy = Math.max(0, event.clientY - startY); // on ne tire que vers le bas\n pane.style.translate = `0 ${dy}px`;\n };\n\n const onEnd = (event: PointerEvent): void => {\n if (!pane || !handle) return;\n cleanupListeners();\n\n const dragged = event.clientY - startY;\n const dismiss = dragged > pane.getBoundingClientRect().height * threshold;\n // WCAG 2.3.3 : sous prefers-reduced-motion, on applique l'état final SANS animation.\n const reduceMotion = !!ownerDoc?.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n\n if (reduceMotion) {\n // draggingClass (transition coupée) encore présente : la translation finale est instantanée,\n // puis on retire la classe (aucun changement de valeur ⇒ aucune animation déclenchée).\n pane.style.translate = dismiss ? '0 100%' : '';\n pane.classList.remove(opts.draggingClass);\n } else {\n pane.classList.remove(opts.draggingClass); // réactive la transition CSS (glissement / snap-back)\n pane.style.translate = dismiss ? '0 100%' : ''; // dismiss : glisse jusqu'en bas ; sinon snap-back à 0\n }\n if (dismiss) opts.onDismiss();\n\n pane = null;\n handle = null;\n ownerDoc = null;\n };\n\n return {\n start(event: PointerEvent): void {\n if (event.button !== 0) return;\n const el = opts.pane();\n if (!el) return;\n handle = event.currentTarget as HTMLElement;\n ownerDoc = handle.ownerDocument;\n // La poignée ne doit PAS voler le focus (sinon un close-on-blur fermerait prématurément).\n event.preventDefault();\n startY = event.clientY;\n pane = el;\n pane.classList.add(opts.draggingClass);\n // Écoute au niveau du document : un pointer qui sort de la poignée (petite cible) ou une\n // capture perdue n'interrompt plus le suivi du glissement ni le snap-back.\n ownerDoc.addEventListener('pointermove', onMove);\n ownerDoc.addEventListener('pointerup', onEnd);\n ownerDoc.addEventListener('pointercancel', onEnd);\n },\n destroy(): void {\n cleanupListeners();\n pane = null;\n handle = null;\n ownerDoc = null;\n },\n };\n}\n","import { DOCUMENT } from '@angular/common';\nimport { Injectable, inject } from '@angular/core';\n\n/**\n * Verrou de défilement du `<body>` à COMPTEUR DE RÉFÉRENCES, partagé (`providedIn: 'root'`).\n *\n * Plusieurs surfaces modales simultanées (sheets, popups) peuvent verrouiller le scroll du fond :\n * `overflow: hidden` n'est posé qu'au premier `lock()` et restauré (à sa valeur d'origine) qu'au\n * dernier `unlock()`. Sans ce compteur, deux consommateurs par-instance se désynchronisent — la\n * fermeture du premier restaure le scroll alors que le second est encore ouvert.\n */\n@Injectable({ providedIn: 'root' })\nexport class KtBodyScrollLock {\n private readonly doc = inject(DOCUMENT);\n private count = 0;\n private previousOverflow = '';\n\n /** Pose un verrou (premier appelant : mémorise puis force `overflow: hidden`). */\n lock(): void {\n if (this.count === 0) {\n this.previousOverflow = this.doc.body.style.overflow;\n this.doc.body.style.overflow = 'hidden';\n }\n this.count++;\n }\n\n /** Relâche un verrou ; restaure l'`overflow` d'origine quand le dernier consommateur relâche. */\n unlock(): void {\n if (this.count === 0) return;\n this.count--;\n if (this.count === 0) {\n this.doc.body.style.overflow = this.previousOverflow;\n }\n }\n}\n","import { InjectionToken, Provider } from '@angular/core';\n\n/**\n * Valeur d'un palier de breakpoint, CONFIGURABLE PAR APPLICATION :\n * - un **nombre** = largeur (px) à partir de laquelle le palier commence ; la lib en dérive une\n * media query LARGEUR SEULE (bornes calculées par rapport au palier suivant) ;\n * - une **chaîne** = media query complète du palier, utilisée VERBATIM (échappatoire pour un critère\n * non-largeur : `orientation`, `pointer`, ratio…).\n */\nexport type KtBreakpointValue = number | string;\n\n/**\n * Seuils responsive de la lib, surchargeables par appli via `provideKtBreakpoints`. Sémantique\n * « plancher de palier » : chaque valeur indique où le palier COMMENCE → 3 bandes mutuellement\n * exclusives (`mobile` < `tablet` < `desktop`).\n */\nexport interface KtBreakpoints {\n /** Plancher du mobile (typiquement 0). */\n mobile: KtBreakpointValue;\n /** Largeur où commence la tablette (= plafond mobile). */\n tablet: KtBreakpointValue;\n /** Largeur où commence le desktop (= plafond tablette). */\n desktop: KtBreakpointValue;\n}\n\n/** Défauts : bascule bottom-sheet sous 600px (aligné sur l'ancien `MOBILE_QUERY`). */\nexport const KT_DEFAULT_BREAKPOINTS: KtBreakpoints = { mobile: 0, tablet: 600, desktop: 1024 };\n\n/** Token des seuils responsive. Défaut = `KT_DEFAULT_BREAKPOINTS` ; surchargé par `provideKtBreakpoints`. */\nexport const KT_BREAKPOINTS = new InjectionToken<KtBreakpoints>('KT_BREAKPOINTS', {\n providedIn: 'root',\n factory: () => KT_DEFAULT_BREAKPOINTS,\n});\n\n/**\n * À ajouter aux `providers` de `app.config.ts`. Fusionne avec les défauts → surcharge partielle OK.\n * @param overrides Seuils à surcharger ; les clés absentes gardent leur défaut `KT_DEFAULT_BREAKPOINTS`.\n * @returns Provider liant `KT_BREAKPOINTS` aux seuils fusionnés.\n * @example provideKtBreakpoints({ tablet: 768, desktop: 1200 })\n * @example provideKtBreakpoints({ tablet: '(min-width: 768px) and (orientation: landscape)' })\n */\nexport function provideKtBreakpoints(overrides: Partial<KtBreakpoints>): Provider {\n return { provide: KT_BREAKPOINTS, useValue: { ...KT_DEFAULT_BREAKPOINTS, ...overrides } };\n}\n\n/** Trois chaînes de media queries résolues, prêtes pour `matchMedia` / `BreakpointObserver`. */\nexport interface KtBreakpointMedia {\n /** Media query de la bande mobile. */\n mobile: string;\n /** Media query de la bande tablette. */\n tablet: string;\n /** Media query de la bande desktop. */\n desktop: string;\n}\n\n/** Évite le chevauchement d'1px entre deux bandes adjacentes (max-width juste sous le plancher suivant). */\nconst EPSILON = 0.02;\n\nfunction minWidth(v: number): string {\n return `(min-width: ${v}px)`;\n}\n\nfunction maxWidth(nextFloor: number): string {\n return `(max-width: ${nextFloor - EPSILON}px)`;\n}\n\n/** `not all` ne matche jamais : repli sûr (= desktop) si une bande numérique se retrouve sans borne. */\nfunction join(...parts: string[]): string {\n return parts.filter(Boolean).join(' and ') || 'not all';\n}\n\n/**\n * Convertit les seuils (nombres et/ou chaînes) en 3 media queries. Un palier-chaîne est repris\n * verbatim ; un palier-nombre devient la bande `[plancher courant .. plancher suivant[`. Si le\n * plancher suivant est une chaîne (pas de borne numérique), la bande reste ouverte de ce côté.\n * @param bp Seuils responsive à convertir (nombres et/ou chaînes).\n * @returns Les trois media queries résolues (`mobile`, `tablet`, `desktop`).\n * @example resolveKtBreakpointMedia(KT_DEFAULT_BREAKPOINTS)\n */\nexport function resolveKtBreakpointMedia(bp: KtBreakpoints): KtBreakpointMedia {\n const { mobile, tablet, desktop } = bp;\n\n return {\n mobile:\n typeof mobile === 'string'\n ? mobile\n : join(mobile > 0 ? minWidth(mobile) : '', typeof tablet === 'number' ? maxWidth(tablet) : ''),\n tablet:\n typeof tablet === 'string'\n ? tablet\n : join(minWidth(tablet), typeof desktop === 'number' ? maxWidth(desktop) : ''),\n desktop: typeof desktop === 'string' ? desktop : minWidth(desktop),\n };\n}\n","import { Injectable, Signal, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { BreakpointObserver } from '@angular/cdk/layout';\nimport { map } from 'rxjs';\nimport { KT_BREAKPOINTS, resolveKtBreakpointMedia } from './breakpoints';\n\n/**\n * Expose l'état responsive courant en SIGNALS, dérivé des seuils configurés par l'appli\n * (`KT_BREAKPOINTS` / `provideKtBreakpoints`). Fondé sur `BreakpointObserver` (`@angular/cdk/layout`,\n * déjà dépendance) : SSR-safe (`matches: false` côté serveur → desktop par défaut) et nettoyage des\n * listeners géré par le CDK.\n *\n * À injecter aussi bien dans les composants de la lib (bascule bottom-sheet) que dans le code des\n * applis consommatrices pour leur propre layout responsive.\n */\n@Injectable({ providedIn: 'root' })\nexport class KtViewport {\n private readonly observer = inject(BreakpointObserver);\n\n /** Media queries résolues (largeur dérivée ou verbatim selon le token) — utile pour miroir CSS. */\n readonly media = resolveKtBreakpointMedia(inject(KT_BREAKPOINTS));\n\n /** Le viewport est dans la bande mobile. */\n readonly isMobile = this.matches(this.media.mobile);\n /** Le viewport est dans la bande tablette. */\n readonly isTablet = this.matches(this.media.tablet);\n /** Le viewport est dans la bande desktop. */\n readonly isDesktop = this.matches(this.media.desktop);\n /** Alias de `isMobile` : palier où les composants de la lib basculent en bottom-sheet. */\n readonly isCompact = this.isMobile;\n\n private matches(query: string): Signal<boolean> {\n return toSignal(this.observer.observe(query).pipe(map((state) => state.matches)), {\n initialValue: false,\n });\n }\n}\n","import { Injectable } from '@angular/core';\n\n/**\n * Génère des identifiants numériques incrémentaux uniques au sein de l'application, soit globalement,\n * soit par préfixe (compteur dédié). Sert à construire des `anchor-name` / `id` stables et distincts.\n *\n * @example\n * ```ts\n * const idGen = inject(KtIdGenerator);\n * const anchor = `--kt-menu-anchor-${idGen.generateId('menu')}`;\n * ```\n */\n@Injectable({\n // Stryker disable next-line all: `providedIn` doit rester statiquement analysable par l'AOT (NG1010).\n providedIn: 'root',\n})\nexport class KtIdGenerator {\n private readonly counters = new Map<string, number>();\n private nextGlobalId = 0;\n\n /**\n * Renvoie le prochain identifiant. Sans `prefix`, utilise le compteur global ; avec `prefix`,\n * utilise un compteur dédié à ce préfixe (séquences indépendantes).\n * @param prefix Clé optionnelle de compteur ; chaque préfixe a sa propre séquence.\n * @returns Le compteur courant (number) pour la portée demandée, avant incrément.\n */\n generateId(prefix?: string): number {\n if (!prefix) {\n return this.nextGlobalId++;\n }\n const current = this.counters.get(prefix) ?? 0;\n this.counters.set(prefix, current + 1);\n return current;\n }\n}\n","import { InjectionToken, isDevMode } from '@angular/core';\n\n/**\n * InjectionToken permettant d'activer ou désactiver les audits d'accessibilité à l'exécution\n * (gardes-fous console.warn en mode dev). Activé par défaut si `isDevMode()` est vrai.\n */\nexport const KT_AUDIT_ENABLED = new InjectionToken<boolean>('KT_AUDIT_ENABLED', {\n providedIn: 'root',\n factory: () => isDevMode(),\n});\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { DialogRef, DEFAULT_DIALOG_CONFIG, CdkDialogContainer, Dialog, DIALOG_DATA } from '@angular/cdk/dialog';
2
2
  import * as i0 from '@angular/core';
3
- import { inject, ElementRef, Renderer2, afterNextRender, Directive, input, isDevMode, DestroyRef, PLATFORM_ID, signal, ChangeDetectionStrategy, Component, ViewContainerRef, Injectable } from '@angular/core';
4
- import { KtIdGenerator, createKtSheetDrag, KtViewport } from '@ktortu/aaa/cdk';
3
+ import { inject, ElementRef, Renderer2, afterNextRender, Directive, input, isDevMode, PLATFORM_ID, DestroyRef, signal, ChangeDetectionStrategy, Component, ViewContainerRef, Injectable } from '@angular/core';
4
+ import { KtIdGenerator, KtBodyScrollLock, KtViewport } from '@ktortu/aaa/cdk';
5
5
  import { isPlatformBrowser } from '@angular/common';
6
6
  import * as i1 from '@angular/cdk/portal';
7
7
  import { PortalModule } from '@angular/cdk/portal';
@@ -255,54 +255,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
255
255
  args: [{ selector: '[ktDialogFocusInitial]' }]
256
256
  }] });
257
257
  /**
258
- * Poignée de préhension d'un dialog en mode `sheet` (bottom-sheet) : drag-to-dismiss vers le bas,
259
- * FACTORISÉ avec le Select via `createKtSheetDrag` (@ktortu/aaa). Geste DOUBLÉ par Échap + bouton
260
- * Fermer + clic sur le scrim (WCAG 2.5.1) décoratif (`aria-hidden`).
261
- *
262
- * À poser sur la barre de préhension, premier enfant du contenu d'un dialog ouvert avec une
263
- * présentation `sheet` / `centered-sheet`. Le drag n'est actif que si le dialog est EFFECTIVEMENT
264
- * en mode sheet (la pane porte `kt-dialog--sheet`) — pas de dépendance au viewport : un `sheet`
265
- * toujours-bottom-sheet se drague aussi à la souris.
266
- *
267
- * @example
268
- * ```html
269
- * <div ktDialogContent>
270
- * <div ktDialogSheetHandle></div>
271
- * …
272
- * </div>
273
- * ```
258
+ * @deprecated ADR-0005 la poignée est désormais RENDUE AUTOMATIQUEMENT par `KtDialogContainer`
259
+ * en présentation `sheet` (opt-out : panelClass additionnel `kt-dialog--no-handle`), et le
260
+ * drag-to-dismiss s'attrape sur TOUTE la surface de la sheet (scroll-snap natif). Cette
261
+ * directive est INERTE et son hôte est masqué (évite une double poignée) : retirez-la de vos
262
+ * templates. Sera supprimée dans une prochaine version majeure.
274
263
  */
275
264
  class KtDialogSheetHandle {
276
- dialogRef = inject(DialogRef, { optional: true });
277
- host = inject(ElementRef).nativeElement;
278
- destroyRef = inject(DestroyRef);
279
- // La feuille translatée = le conteneur CDK (position:fixed en mode sheet), trouvé en remontant.
280
- drag = createKtSheetDrag({
281
- pane: () => this.host.closest('.cdk-dialog-container'),
282
- onDismiss: () => this.dialogRef?.close(),
283
- draggingClass: 'kt-dialog-sheet--dragging',
284
- });
285
265
  constructor() {
286
- this.destroyRef.onDestroy(() => this.drag.destroy());
287
- }
288
- onStart(event) {
289
- // Actif seulement quand la PRÉSENTATION choisie par le dev est `sheet` (classe sur la pane).
290
- const pane = this.host.closest('.cdk-overlay-pane');
291
- if (!pane?.classList.contains('kt-dialog--sheet'))
292
- return;
293
- this.drag.start(event);
266
+ if (isDevMode()) {
267
+ console.warn('[ktDialogSheetHandle] déprécié (ADR-0005) : la poignée est rendue automatiquement en ' +
268
+ 'présentation sheet et le drag s’attrape partout — retirez la directive de votre template.');
269
+ }
294
270
  }
295
271
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtDialogSheetHandle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
296
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: KtDialogSheetHandle, isStandalone: true, selector: "[ktDialogSheetHandle]", host: { attributes: { "aria-hidden": "true" }, listeners: { "pointerdown": "onStart($event)", "mousedown": "$event.preventDefault()" } }, ngImport: i0 });
272
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: KtDialogSheetHandle, isStandalone: true, selector: "[ktDialogSheetHandle]", host: { attributes: { "aria-hidden": "true" }, styleAttribute: "display: none" }, ngImport: i0 });
297
273
  }
298
274
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtDialogSheetHandle, decorators: [{
299
275
  type: Directive,
300
276
  args: [{
301
277
  selector: '[ktDialogSheetHandle]',
302
278
  host: {
303
- '(pointerdown)': 'onStart($event)',
304
- '(mousedown)': '$event.preventDefault()',
305
279
  'aria-hidden': 'true',
280
+ style: 'display: none',
306
281
  },
307
282
  }]
308
283
  }], ctorParameters: () => [] });
@@ -331,6 +306,12 @@ const KT_DIALOG_AAA_DEFAULTS = {
331
306
  hasBackdrop: true,
332
307
  panelClass: 'kt-dialog',
333
308
  backdropClass: 'kt-dialog__backdrop',
309
+ /* Plafond de largeur posé en STYLE INLINE sur le pane par l'overlay (canal prévu par le CDK).
310
+ Indispensable : le CDK injecte à l'exécution `.cdk-overlay-pane { max-width: 100% }` HORS
311
+ layer, qui bat toute règle de la lib importée via layer() — seul un style inline gagne.
312
+ La custom property garde la thémisation ; `--fullscreen`/`--sheet` la retunent sur leur
313
+ pane (cf. dialog.css). */
314
+ maxWidth: 'var(--dialog-max-width)',
334
315
  };
335
316
  /**
336
317
  * Enregistre les valeurs par défaut AAA du dialog au niveau application.
@@ -391,8 +372,16 @@ class KtDialogContainer extends CdkDialogContainer {
391
372
  elementRef = inject(ElementRef);
392
373
  host = this.elementRef.nativeElement;
393
374
  platformId = inject(PLATFORM_ID);
375
+ destroyRef = inject(DestroyRef);
376
+ bodyScrollLock = inject(KtBodyScrollLock);
394
377
  isClosing = signal(false, /* @ts-ignore */
395
378
  ...(ngDevMode ? [{ debugName: "isClosing" }] : /* istanbul ignore next */ []));
379
+ // --- Sheet scroll-snap (ADR-0005) : détection du snap « fermé » + fermeture différée ---
380
+ /** Armé quand l'ouverture a dépassé 50px : un repos à ~0 est alors forcément le snap « fermé ». */
381
+ sheetArmed = false;
382
+ /** Fermeture réelle en attente du repos au snap « fermé » (sortie programmatique animée). */
383
+ pendingSheetClose = null;
384
+ sheetCloseTimer;
396
385
  /** Premier id de la file `aria-labelledby` exposée par `CdkDialogContainer`. Le membre
397
386
  `_ariaLabelledByQueue` est interne au CDK (préfixe `_`) : on l'encapsule ICI, gardé, pour que
398
387
  le binding de template ne lise jamais l'interne directement. Si une montée du CDK retire ou
@@ -416,6 +405,8 @@ class KtDialogContainer extends CdkDialogContainer {
416
405
  // - aria-describedby orphelin → on RETIRE l'attribut (pas de description fournie) ;
417
406
  // - aucun nom accessible (ni titre ni aria-label) → warn dev (WCAG 4.1.2).
418
407
  afterNextRender(() => {
408
+ if (this.isSheet())
409
+ this.initSheetGesture();
419
410
  const doc = this.host.ownerDocument;
420
411
  const describedBy = this.host.getAttribute('aria-describedby');
421
412
  if (describedBy && !doc.getElementById(describedBy)) {
@@ -433,6 +424,82 @@ class KtDialogContainer extends CdkDialogContainer {
433
424
  isSheet() {
434
425
  return this._config.panelClass?.includes('kt-dialog--sheet') ?? false;
435
426
  }
427
+ /** Poignée décorative auto-rendue en présentation sheet. Opt-out : panelClass `kt-dialog--no-handle`. */
428
+ showSheetHandle() {
429
+ return !this._config.panelClass?.includes('kt-dialog--no-handle');
430
+ }
431
+ /** Tap sur le spacer (zone extérieure à la carte) = fermeture, façon backdrop. Le conteneur
432
+ captant désormais le tactile (pointer-events auto, requis pour le drag iOS), le clic ne
433
+ traverse plus vers le backdrop CDK : on porte donc ICI la fermeture au tap-extérieur, en
434
+ respectant `disableClose` (comme le faisait le backdropClick natif). Un drag n'émet pas de
435
+ `click` (le navigateur le supprime après un scroll) : seul un vrai tap ferme. */
436
+ onScrimClick() {
437
+ if (this._config.disableClose || this.isClosing())
438
+ return;
439
+ this.dialogRef.close();
440
+ }
441
+ /** Sheet scroll-snap : écouteurs (détection du repos + garde molette) et scroll d'ENTRÉE
442
+ programmatique vers le snap « ouvert ». Appelé au premier rendu, en présentation sheet. */
443
+ initSheetGesture() {
444
+ const host = this.host;
445
+ host.addEventListener('scroll', this.onSheetScroll);
446
+ host.addEventListener('wheel', this.onSheetWheel, { passive: false });
447
+ // Verrou de scroll du fond (compteur partagé) : sans lui, le fond défile SOUS la sheet et,
448
+ // sur iOS, le geste de dismiss déclenche le pull-to-refresh de la page. La stratégie `block`
449
+ // du CDK ne suffit pas ici (non appliquée / non fiable sur iOS) — même approche que le Select.
450
+ this.bodyScrollLock.lock();
451
+ this.destroyRef.onDestroy(() => {
452
+ clearTimeout(this.sheetCloseTimer);
453
+ host.removeEventListener('scroll', this.onSheetScroll);
454
+ host.removeEventListener('wheel', this.onSheetWheel);
455
+ this.bodyScrollLock.unlock();
456
+ });
457
+ host.scrollTop = 0;
458
+ requestAnimationFrame(() => {
459
+ host.scrollTo?.({
460
+ top: host.scrollHeight - host.clientHeight,
461
+ behavior: this.prefersReducedMotion() ? 'auto' : 'smooth',
462
+ });
463
+ });
464
+ }
465
+ prefersReducedMotion() {
466
+ return this.host.ownerDocument.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
467
+ }
468
+ /** Détection du snap « fermé » par position de scroll (ni scrollsnapchange ni scrollend — cf.
469
+ ADR-0005). Un repos à ~0 finalise la fermeture en attente, ou EST une fermeture par geste. */
470
+ onSheetScroll = () => {
471
+ const top = this.host.scrollTop;
472
+ if (top > 50)
473
+ this.sheetArmed = true;
474
+ if (!this.sheetArmed || top > 1)
475
+ return;
476
+ const pending = this.pendingSheetClose;
477
+ if (pending) {
478
+ this.pendingSheetClose = null;
479
+ clearTimeout(this.sheetCloseTimer);
480
+ pending();
481
+ return;
482
+ }
483
+ // Dismissal par geste : transite par le close intercepté (animation déjà « jouée » par le doigt).
484
+ if (!this.isClosing())
485
+ this.dialogRef.close();
486
+ };
487
+ /** La molette ne ferme JAMAIS la sheet (pas de geste souris — ADR-0005) : seuls les scrollers
488
+ INTERNES peuvent consommer, dans leurs bornes (générique : un dialog peut imbriquer
489
+ plusieurs zones scrollables). */
490
+ onSheetWheel = (event) => {
491
+ let el = event.target;
492
+ while (el && el !== this.host) {
493
+ if (el.scrollHeight > el.clientHeight + 1) {
494
+ const canScrollDown = el.scrollTop + el.clientHeight < el.scrollHeight - 1;
495
+ const canScrollUp = el.scrollTop > 0;
496
+ if ((event.deltaY > 0 && canScrollDown) || (event.deltaY < 0 && canScrollUp))
497
+ return;
498
+ }
499
+ el = el.parentElement;
500
+ }
501
+ event.preventDefault();
502
+ };
436
503
  animateAndClose(result, originalCloseFn) {
437
504
  if (this.isClosing())
438
505
  return;
@@ -441,17 +508,37 @@ class KtDialogContainer extends CdkDialogContainer {
441
508
  originalCloseFn(result);
442
509
  return;
443
510
  }
444
- // Récupérer la durée de transition configurée en CSS (ex: "150ms" ou "0.2s")
445
- const styles = window.getComputedStyle(this.host);
446
- const durationStr = styles.transitionDuration || '0s';
511
+ // Sheet scroll-snap : la sortie est un scroll programmatique vers le snap « fermé » ;
512
+ // onSheetScroll finalise au repos. Déjà en bas (geste abouti, jsdom, reduced-motion) :
513
+ // fermeture immédiate. Fallback : timeout (scroll lisse interrompu).
514
+ if (this.isSheet()) {
515
+ if (this.host.scrollTop <= 1) {
516
+ originalCloseFn(result);
517
+ return;
518
+ }
519
+ this.sheetArmed = true;
520
+ this.pendingSheetClose = () => originalCloseFn(result);
521
+ this.host.scrollTo?.({ top: 0, behavior: this.prefersReducedMotion() ? 'auto' : 'smooth' });
522
+ clearTimeout(this.sheetCloseTimer);
523
+ this.sheetCloseTimer = setTimeout(() => {
524
+ const pending = this.pendingSheetClose;
525
+ this.pendingSheetClose = null;
526
+ pending?.();
527
+ }, 400);
528
+ return;
529
+ }
530
+ // Récupérer la durée de transition configurée en CSS (ex: "150ms" ou "0.2s"). Vue lue via
531
+ // ownerDocument.defaultView (pas le global `window`) : cohérent avec prefersReducedMotion() et
532
+ // sûr indépendamment du garde de plateforme — si la vue manque, durée 0 → fermeture immédiate.
533
+ const durationStr = this.host.ownerDocument.defaultView?.getComputedStyle(this.host).transitionDuration || '0s';
447
534
  const durationMs = parseFloat(durationStr) * (durationStr.includes('ms') ? 1 : 1000);
448
535
  if (durationMs === 0) {
449
536
  originalCloseFn(result);
450
537
  return;
451
538
  }
452
- // Écouter la fin de la transition sur la propriété 'translate'
539
+ // Écouter la fin de la transition (opacity pour le dialog classique, translate pour la sheet)
453
540
  const onTransitionEnd = (event) => {
454
- if (event.target === this.host && event.propertyName === 'translate') {
541
+ if (event.target === this.host && (event.propertyName === 'translate' || event.propertyName === 'opacity')) {
455
542
  this.host.removeEventListener('transitionend', onTransitionEnd);
456
543
  originalCloseFn(result);
457
544
  }
@@ -464,8 +551,21 @@ class KtDialogContainer extends CdkDialogContainer {
464
551
  }, durationMs + 50);
465
552
  }
466
553
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtDialogContainer, deps: [], target: i0.ɵɵFactoryTarget.Component });
467
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.1", type: KtDialogContainer, isStandalone: true, selector: "kt-dialog-container", host: { attributes: { "tabindex": "-1" }, properties: { "attr.id": "_config.id || null", "attr.role": "_config.role", "attr.aria-modal": "_config.ariaModal", "attr.aria-labelledby": "_config.ariaLabel ? null : ariaLabelledBy", "attr.aria-label": "_config.ariaLabel", "attr.aria-describedby": "_config.ariaDescribedBy || null", "class.kt-dialog-container--sheet": "isSheet()", "class.kt-dialog-container--closing": "isClosing()" }, classAttribute: "cdk-dialog-container kt-dialog-container" }, usesInheritance: true, ngImport: i0, template: `
554
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtDialogContainer, isStandalone: true, selector: "kt-dialog-container", host: { attributes: { "tabindex": "-1" }, properties: { "attr.id": "_config.id || null", "attr.role": "_config.role", "attr.aria-modal": "_config.ariaModal", "attr.aria-labelledby": "_config.ariaLabel ? null : ariaLabelledBy", "attr.aria-label": "_config.ariaLabel", "attr.aria-describedby": "_config.ariaDescribedBy || null", "class.kt-dialog-container--sheet": "isSheet()", "class.kt-dialog-container--closing": "isClosing()" }, classAttribute: "cdk-dialog-container kt-dialog-container" }, usesInheritance: true, ngImport: i0, template: `
555
+ <!-- Sheet scroll-snap (ADR-0005) : le conteneur est le SCROLLER, le spacer porte le snap
556
+ « fermé » (un écran) et la carte (layout) le snap « ouvert ». -->
557
+ @if (isSheet()) {
558
+ <!-- Spacer = zone de tap-extérieur (ferme, comme un backdrop) ET surface de drag (scroll natif).
559
+ Le conteneur capte le tactile (pointer-events auto) : le clic ne traverse plus vers le
560
+ backdrop CDK, la fermeture au tap est donc portée ici (disableClose respecté). -->
561
+ <div class="kt-dialog-container__spacer" aria-hidden="true" (click)="onScrimClick()"></div>
562
+ }
468
563
  <div class="kt-dialog-container__layout">
564
+ @if (isSheet() && showSheetHandle()) {
565
+ <!-- Poignée DÉCORATIVE auto-rendue : la sheet s'attrape partout (opt-out :
566
+ panelClass additionnel 'kt-dialog--no-handle' à l'ouverture). -->
567
+ <div class="kt-dialog-container__sheet-handle" aria-hidden="true"></div>
568
+ }
469
569
  <ng-template cdkPortalOutlet></ng-template>
470
570
  </div>
471
571
  `, isInline: true, dependencies: [{ kind: "ngmodule", type: PortalModule }, { kind: "directive", type: i1.CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
@@ -489,7 +589,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
489
589
  '[class.kt-dialog-container--closing]': 'isClosing()',
490
590
  },
491
591
  template: `
592
+ <!-- Sheet scroll-snap (ADR-0005) : le conteneur est le SCROLLER, le spacer porte le snap
593
+ « fermé » (un écran) et la carte (layout) le snap « ouvert ». -->
594
+ @if (isSheet()) {
595
+ <!-- Spacer = zone de tap-extérieur (ferme, comme un backdrop) ET surface de drag (scroll natif).
596
+ Le conteneur capte le tactile (pointer-events auto) : le clic ne traverse plus vers le
597
+ backdrop CDK, la fermeture au tap est donc portée ici (disableClose respecté). -->
598
+ <div class="kt-dialog-container__spacer" aria-hidden="true" (click)="onScrimClick()"></div>
599
+ }
492
600
  <div class="kt-dialog-container__layout">
601
+ @if (isSheet() && showSheetHandle()) {
602
+ <!-- Poignée DÉCORATIVE auto-rendue : la sheet s'attrape partout (opt-out :
603
+ panelClass additionnel 'kt-dialog--no-handle' à l'ouverture). -->
604
+ <div class="kt-dialog-container__sheet-handle" aria-hidden="true"></div>
605
+ }
493
606
  <ng-template cdkPortalOutlet></ng-template>
494
607
  </div>
495
608
  `,